diff --git a/.codacy.yml b/.codacy.yml index d98077f4fe..cf0315d765 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -1,3 +1,11 @@ --- +engines: + tsqllint: + exclude_paths: + # These fixtures use PostgreSQL grammar, not Transact-SQL. + - "src/test/resources/postgresql/**" + # MATCH_RECOGNIZE fixtures use Oracle and BigQuery grammar. + - "src/test/resources/net/sf/jsqlparser/statement/select/match-recognize/**" + - "src/test/resources/net/sf/jsqlparser/statement/select/match-recognize-2350.sql" exclude_paths: - "site/**" diff --git a/.github/requirements-docs.txt b/.github/requirements-docs.txt new file mode 100644 index 0000000000..9130aea185 --- /dev/null +++ b/.github/requirements-docs.txt @@ -0,0 +1,14 @@ +# Python dependencies for the Sphinx documentation build (.github/workflows/ci.yml). +# Kept in the repository so actions/setup-python can key its pip cache on it. +# +# Deliberately NOT installed from apt: sphinx-common pulls python3-pil / python3-docutils +# from the runner's stale apt index and shadows this install on PATH. + +sphinx>=7,<9 +manticore_sphinx_theme +sphinx_javadoc_xml +myst_parser +sphinx_substitution_extensions +sphinx_issues +sphinx_inline_tabs +pygments diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3108f35b5..569d885551 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,103 +2,218 @@ name: CI Pipeline on: push: - branches: [ "**" ] # Run on every commit to any branch + branches: [ "**" ] pull_request: - branches: [ "**" ] # Run for PRs from any branch + branches: [ "**" ] workflow_dispatch: -permissions: write-all +# Least privilege by default; each job widens only what it needs. +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + JAVA_VERSION: '17' + JAVA_DISTRIBUTION: 'temurin' jobs: + grammar_conflicts: + name: Grammar Choice Conflicts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v5 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.ref != 'refs/heads/master' }} + + # Re-runs JavaCC on the JJTree output and fails on any "Warning: Choice conflict ...". + # `check` already depends on this task, but a dedicated job gives a fast, separately + # named status check that can be made required in branch protection. + - name: Check for grammar ambiguities + run: ./gradlew checkGrammarAmbiguity --no-daemon + + - name: Upload grammar warning report + if: failure() + uses: actions/upload-artifact@v5 + with: + name: javacc-warnings + path: build/reports/grammar/ + retention-days: 7 + if-no-files-found: ignore + gradle_check: - name: Gradle Check + name: Gradle Check (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] steps: - - uses: actions/checkout@main + - uses: actions/checkout@v5 with: - fetch-depth: 0 + fetch-depth: 0 # required by the git-changelog / version derivation - - name: Set up JDK 17 - uses: actions/setup-java@main + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v5 with: - java-version: '17' - distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Set up Gradle - uses: gradle/actions/setup-gradle@main + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.ref != 'refs/heads/master' }} - name: Run Gradle Check - run: ./gradlew check + run: ./gradlew check --no-daemon + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v5 + with: + name: test-reports-${{ matrix.os }} + path: | + build/reports/ + build/test-results/ + retention-days: 7 + if-no-files-found: ignore maven_verify: - name: Maven Verify - needs: gradle_check # ✅ Run only after Gradle check succeeds + name: Maven Verify (${{ matrix.os }}) + needs: gradle_check runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: -# os: [ ubuntu-latest, windows-latest, macos-latest ] - os: [ ubuntu-latest, macos-latest ] + os: [ ubuntu-latest, windows-latest, macos-latest ] steps: - - uses: actions/checkout@main + - uses: actions/checkout@v5 with: fetch-depth: 0 - - name: Set up JDK 17 - uses: actions/setup-java@main + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v5 with: - java-version: '17' - distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + cache: 'maven' - name: Run Maven Verify - run: mvn --batch-mode verify + run: mvn --batch-mode --no-transfer-progress verify gradle_publish: - 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 + name: Gradle Publish (Maven Central) + needs: [ gradle_check, maven_verify ] + if: github.ref == 'refs/heads/master' && github.repository == 'JSQLParser/JSqlParser' runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@main + - uses: actions/checkout@v5 with: fetch-depth: 0 - - name: Set up JDK 17 - uses: actions/setup-java@main + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v5 with: - java-version: '17' - distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} - - name: Build with Gradle - uses: gradle/actions/setup-gradle@main + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 - name: Publish with Gradle - run: ./gradlew publish + # Gradle only picks up a secret as a project property when it is exported with the + # ORG_GRADLE_PROJECT_ prefix. Plain `ossrhUsername: ...` was an ordinary environment + # variable that nothing ever read. + # + # No RELEASE variable here on purpose: getVersion() then produces a -SNAPSHOT version and + # the plugin routes it to the Central Portal snapshot repository. Set RELEASE=true only + # when you actually intend to cut a release. + run: ./gradlew publishToMavenCentral --no-daemon env: - ossrhUsername: ${{ secrets.OSSRHUSERNAME }} - ossrhPassword: ${{ secrets.OSSRHPASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.mavenCentralUsername }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.mavenCentralPassword }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.signingInMemoryKey }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.signingInMemoryKeyId }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.signingInMemoryKeyPassword }} + + deploy_docs: + name: Build & Deploy Sphinx Docs + needs: [ gradle_check, maven_verify ] + if: github.ref == 'refs/heads/master' && github.repository == 'JSQLParser/JSqlParser' + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 # gitChangelogTask needs full history - - uses: actions/setup-python@main + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v5 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} - - name: Install XSLT Processor - run: sudo apt-get install -y xsltproc sphinx-common + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + # cache: 'pip' needs a dependency file to key on. setup-python only looks for + # **/requirements.txt or **/pyproject.toml by default, neither of which exists in this + # repo — hence "No file ... matched". cache-dependency-path points it at the file we keep. + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + cache: 'pip' + cache-dependency-path: '.github/requirements-docs.txt' + + # Only xsltproc comes from apt. `sphinx-common` used to be installed here as well; it + # pulled in python3-sphinx / python3-docutils / python3-pil from the runner's stale apt + # index (hence the 404 on python3-pil 10.2.0-1ubuntu1.2) and shadowed the pip-installed + # Sphinx that actually knows about manticore_sphinx_theme. `apt-get update` first, because + # the pre-baked index on the runner image is regularly older than the mirror. + - name: Install XSLT processor + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends xsltproc - name: Install Python dependencies - run: pip install manticore_sphinx_theme sphinx_javadoc_xml myst_parser sphinx_substitution_extensions sphinx_issues sphinx_inline_tabs pygments + run: | + python -m pip install --upgrade pip + python -m pip install -r .github/requirements-docs.txt - name: Build Sphinx documentation with Gradle - run: FLOATING_TOC=false ./gradlew -DFLOATING_TOC=false gitChangelogTask renderRR xslt xmldoc sphinx + env: + FLOATING_TOC: 'false' + run: ./gradlew -DFLOATING_TOC=false --no-daemon gitChangelogTask renderRR xslt xmldoc sphinx - name: Configure GitHub Pages - uses: actions/configure-pages@main + uses: actions/configure-pages@v5 - name: Upload artifact - uses: actions/upload-pages-artifact@main + uses: actions/upload-pages-artifact@v5 with: path: 'build/sphinx' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@main + uses: actions/deploy-pages@v5 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 955e7bf2da..bab2fa1408 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,11 @@ /src/site/sphinx/_themes # Exclude the Auto-generated Changelog -/src/site/sphinx/changelog.rst -/src/site/sphinx/syntax_stable.rst -/src/site/sphinx/syntax_snapshot.rst -/src/site/sphinx/javadoc_stable.xml -/src/site/sphinx/javadoc_snapshot.xml +src/site/sphinx/changelog.rst +src/site/sphinx/syntax_stable.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/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000000..7fecadb38e --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1 @@ +-Dfile.encoding=UTF-8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3637519ab1..4d354ae2ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,37 +2,909 @@ Changelog of JSqlParser. -## jsqlparser-5.4 (2025-05-25) +## jsqlparser-5.4 (2026-09-13) ### Features -- Session Statement ([7d2e6](https://github.com/JSQLParser/JSqlParser/commit/7d2e6b65324ce57) manticore-projects) -- sync with Master ([e14d7](https://github.com/JSQLParser/JSqlParser/commit/e14d7eb1c4e9963) manticore-projects) -- JavaCC 8 keyword utils ([cfe2d](https://github.com/JSQLParser/JSqlParser/commit/cfe2d8ccaf7c76d) manticore-projects) -- Complete on JavaCC-8 ([1b7ed](https://github.com/JSQLParser/JSqlParser/commit/1b7ed2d7be000ce) manticore-projects) -- Optimise performance ([e91c4](https://github.com/JSQLParser/JSqlParser/commit/e91c480b0bbe0a9) manticore-projects) -- avoid looping through the tokens every single time ([b18fb](https://github.com/JSQLParser/JSqlParser/commit/b18fbca1f48e63e) manticore-projects) -- avoid looping through the tokens every single time ([7ac6c](https://github.com/JSQLParser/JSqlParser/commit/7ac6cd0fa08d713) manticore-projects) -- add proper JMH benchmarks ([21c98](https://github.com/JSQLParser/JSqlParser/commit/21c983fc1f4f3f2) manticore-projects) -- JavaCC-8 ([9d144](https://github.com/JSQLParser/JSqlParser/commit/9d1442e9a4800e2) manticore-projects) -- remove all semantic lookaheads ([5abca](https://github.com/JSQLParser/JSqlParser/commit/5abcaeaede27cfe) manticore-projects) +- parse SQL large-object type names through shared column types (#2617) ([ed6c4](https://github.com/JSQLParser/JSqlParser/commit/ed6c499277b4343) Minjae Lee) +- preserve column default expressions in table definitions (#2616) ([2aa0d](https://github.com/JSQLParser/JSqlParser/commit/2aa0d8dab6e04ee) Minjae Lee) +- reuse table constraint grammar for PostgreSQL ALTER ADD (#2612) ([e4cf3](https://github.com/JSQLParser/JSqlParser/commit/e4cf31654c4b0e6) Minjae Lee) +- preserve ALTER COLUMN default expressions in the AST (#2611) ([08de1](https://github.com/JSQLParser/JSqlParser/commit/08de14159c1f775) Minjae Lee) +- **parser** support opt-in legacy MySQL GROUP BY ordering (#2595) ([7cc86](https://github.com/JSQLParser/JSqlParser/commit/7cc863863170c79) Minjae Lee) +- **parser** model PostgreSQL type domain and extension DDL (#2553) ([7e15a](https://github.com/JSQLParser/JSqlParser/commit/7e15aaedfb06558) Minjae Lee) +- **parser** model PostgreSQL schema DDL clauses (#2552) ([7b9d4](https://github.com/JSQLParser/JSqlParser/commit/7b9d4208d5e42ad) Minjae Lee) +- **parser** parse navigation chains after array subscripts (#2550) ([54a74](https://github.com/JSQLParser/JSqlParser/commit/54a7499579c6bbf) fudian) +- **statement** add StmtFeature classification via Statement#getFeatures() ([22023](https://github.com/JSQLParser/JSqlParser/commit/22023a48404140e) Andreas Reichel) +- **parser** enhance PostgreSQL CREATE POLICY support (#2549) ([ff2bc](https://github.com/JSQLParser/JSqlParser/commit/ff2bc8d29a00a8c) Minjae Lee) +- **parser** model MySQL CREATE TRIGGER (#2548) ([03b8f](https://github.com/JSQLParser/JSqlParser/commit/03b8fc407b32ca3) Minjae Lee) +- **parser** model MySQL event statements (#2547) ([b67a5](https://github.com/JSQLParser/JSqlParser/commit/b67a52d0f5e4531) Minjae Lee) +- **parser** model MySQL CREATE USER (#2546) ([628e3](https://github.com/JSQLParser/JSqlParser/commit/628e3c1d08fd62c) Minjae Lee) +- **parser** model MySQL schema options (#2544) ([897fb](https://github.com/JSQLParser/JSqlParser/commit/897fbd4c6549e3b) Minjae Lee) +- **parser** model ColDataType precision and scale as structured properties (#2539) ([c919a](https://github.com/JSQLParser/JSqlParser/commit/c919a0c9b316c8d) Minjae Lee) +- **parser** add column data type fragment parser (#2531) ([6413a](https://github.com/JSQLParser/JSqlParser/commit/6413a84724cc309) Minjae Lee) +- **parser** add BIGQUERY, DATABRICKS and SNOWFLAKE dialect presets (#2515) ([e7167](https://github.com/JSQLParser/JSqlParser/commit/e7167cc7dc15ce5) fudian) +- **parser** concatenate adjacent string literals behind adjacentStringLiterals (#2514) ([84d23](https://github.com/JSQLParser/JSqlParser/commit/84d23e7dfae5ac8) fudian) +- **parser** support double quoted string literals behind allowDoubleQuotedStrings (#2513) ([c86cf](https://github.com/JSQLParser/JSqlParser/commit/c86cf6ad2ce210b) fudian) +- **parser** turn on the dialect's lexer feature preset in withDialect (#2511) ([94c45](https://github.com/JSQLParser/JSqlParser/commit/94c4508bd381912) fudian) +- **parser** support MySQL # line comments behind allowHashLineComments (#2508) ([177db](https://github.com/JSQLParser/JSqlParser/commit/177dbd716f0a3c3) fudian) +- **parser** support DuckDB MAP literals and PIVOT queries (#2496) ([e1301](https://github.com/JSQLParser/JSqlParser/commit/e13019a393d9229) Liming Deng) +- **parser** support MySQL PROCEDURE ANALYSE clause (#2492) ([98bd0](https://github.com/JSQLParser/JSqlParser/commit/98bd01de38de073) Liming Deng) +- parse BigQuery JSON string literals as implicit casts (#2488) ([15e58](https://github.com/JSQLParser/JSqlParser/commit/15e58e92e1a9692) Hayssam Saleh) +- complete SQL Server OPTION query hint support (#2486) ([4c6a4](https://github.com/JSQLParser/JSqlParser/commit/4c6a4fb141db7db) fudian) +- **parser** add support for ClickHouse [LEFT] ARRAY JOIN syntax (#2482) ([5c868](https://github.com/JSQLParser/JSqlParser/commit/5c86877dfa8f3d3) Watashi00) +- support CREATE DATABASE statement (#2070) (#2471) ([7b648](https://github.com/JSQLParser/JSqlParser/commit/7b648250618299e) fudian) +- support SQL Server OPTION query hints (#161) (#2472) ([13894](https://github.com/JSQLParser/JSqlParser/commit/138947d6b1d0d28) fudian) +- add support for the XMLTABLE table function (#2458) ([d51ee](https://github.com/JSQLParser/JSqlParser/commit/d51ee1c166055d3) fudian) +- support MERGE ... WHEN NOT MATCHED BY TARGET / BY SOURCE (#2421) (#2453) ([25f30](https://github.com/JSQLParser/JSqlParser/commit/25f3040e3938fb3) fudian) +- add ForUpdateClause class with multi-table and ORDER BY support (#2426) ([2b141](https://github.com/JSQLParser/JSqlParser/commit/2b141568e8b76ed) Rogerio Robetti) +- Add bytecode size verification tests for generated parser classes ([9cfbe](https://github.com/JSQLParser/JSqlParser/commit/9cfbe681e36e638) manticore-projects) +- Optimize SimpleCharStream for SQL parsing hot path ([17448](https://github.com/JSQLParser/JSqlParser/commit/17448bb074e6371) manticore-projects) +- PRATT 7.1 ms/ops, no errors ([2d2ee](https://github.com/JSQLParser/JSqlParser/commit/2d2ee4b569e45df) manticore-projects) +- Pratt 8.5 ms/ops without failures ([36507](https://github.com/JSQLParser/JSqlParser/commit/365078db38dfa14) manticore-projects) +- Pratt 6.5 ms/ops with 14 failures ([53ca0](https://github.com/JSQLParser/JSqlParser/commit/53ca03861bb93b2) manticore-projects) +- rework tokens and preserved keywords ([08d0b](https://github.com/JSQLParser/JSqlParser/commit/08d0bcc9cb0efdf) manticore-projects) +- Allow more Function keyword arguments ([cd71a](https://github.com/JSQLParser/JSqlParser/commit/cd71aada3897745) manticore-projects) +- Fix GROUP_CONCAT SEPARATOR to accept expressions, not just string literals ([ff28f](https://github.com/JSQLParser/JSqlParser/commit/ff28f826bcf89c3) manticore-projects) +- support PostgreSQL 18 RETURNING OLD/NEW (#2398) ([f47a8](https://github.com/JSQLParser/JSqlParser/commit/f47a8b30b303e76) Liming Deng) +- support EXPLAIN for DML and simplify ExplainStatement (#2396) ([b19d5](https://github.com/JSQLParser/JSqlParser/commit/b19d556eb57f701) Liming Deng) +- add support for Oracle INSERT ALL/FIRST with WHEN branches (#2394) ([4f982](https://github.com/JSQLParser/JSqlParser/commit/4f982e74aee7f12) Liming Deng) +- add PostgreSQL Row Level Security (RLS) support (#2345) ([999cd](https://github.com/JSQLParser/JSqlParser/commit/999cdca2ac1be89) Eyal Gehasie) +- `DateUnitExpression` for parsing `HOUR`, `DAY`, `MONTH` etc. in Date Functions not as column ([4fdfa](https://github.com/JSQLParser/JSqlParser/commit/4fdfa785dcfd2d5) Andreas Reichel) +- normalised backtick quotes ([c60ff](https://github.com/JSQLParser/JSqlParser/commit/c60ff73909717be) Andreas Reichel) +- normalised backtick quotes ([619f8](https://github.com/JSQLParser/JSqlParser/commit/619f8dadb6ce942) Andreas Reichel) +- `TRY_CONVERT` and `SAFE_CONVERT` support ([9dfa0](https://github.com/JSQLParser/JSqlParser/commit/9dfa0d68af7abd3) Andreas Reichel) +- `SessionStatement` with options ([6c98f](https://github.com/JSQLParser/JSqlParser/commit/6c98f10f2d83342) Andreas Reichel) +- `SessionStatement` with options ([595d4](https://github.com/JSQLParser/JSqlParser/commit/595d4efa32f73b7) Andreas Reichel) +- `SessionStatement` with options ([55a6c](https://github.com/JSQLParser/JSqlParser/commit/55a6c465e49aeb2) Andreas Reichel) +- split catalog and schema ([4ff5c](https://github.com/JSQLParser/JSqlParser/commit/4ff5cc9830ef2e3) Andreas Reichel) +- split catalog and schema ([55201](https://github.com/JSQLParser/JSqlParser/commit/552019a56e30690) Andreas Reichel) +- `CREATE SCHEMA` with catalog ([ac46c](https://github.com/JSQLParser/JSqlParser/commit/ac46c4346eafcf8) Andreas Reichel) +- enable session with catalog ([30144](https://github.com/JSQLParser/JSqlParser/commit/30144e72ab38ccd) Andreas Reichel) +- **parser** allow COLLATE in ORDER BY clauses (issue #2245) (#2277) ([8810c](https://github.com/JSQLParser/JSqlParser/commit/8810c016c3c83ea) Sam Sovereign) +- Oracle hierarchical queries to except `Expression` in the operator ([624a7](https://github.com/JSQLParser/JSqlParser/commit/624a768b2eeae67) Andreas Reichel) +- `CORRESPONDING` modifier for `SetOperator` ([5fe93](https://github.com/JSQLParser/JSqlParser/commit/5fe938bc369a83b) Andreas Reichel) +- add support for DuckDB `CREATE TABLE` with `STRUCT(..)` columns ([0e171](https://github.com/JSQLParser/JSqlParser/commit/0e1715e9b07f293) Andreas Reichel) +- BigQuery `Historic Version` syntax ([5fa07](https://github.com/JSQLParser/JSqlParser/commit/5fa071ef240a82b) manticore-projects) +- Databricks `Temporal spec` syntax ([df5e6](https://github.com/JSQLParser/JSqlParser/commit/df5e6690ea24485) manticore-projects) +- Snowflake time travel syntax ([6f4c4](https://github.com/JSQLParser/JSqlParser/commit/6f4c4fb2dd441e4) manticore-projects) +- syntax sugar ([7c371](https://github.com/JSQLParser/JSqlParser/commit/7c371718678ee78) manticore-projects) +- more useful VisitorAdaptors ([7047c](https://github.com/JSQLParser/JSqlParser/commit/7047c187f85fcf1) manticore-projects) +- more useful VisitorAdaptors ([c6278](https://github.com/JSQLParser/JSqlParser/commit/c62781a3e16dafb) manticore-projects) +- make the various Visitor Adaptors more useful ([066e1](https://github.com/JSQLParser/JSqlParser/commit/066e1bbb7335aa2) manticore-projects) +- better defaults for `FromItemVisitorAdaptor` ([7feb3](https://github.com/JSQLParser/JSqlParser/commit/7feb3bc18a305f0) manticore-projects) +- support distinctrow keyword (#2238) ([e17cd](https://github.com/JSQLParser/JSqlParser/commit/e17cdef4275fa2b) Emily Ong) +- add Features for `dialect` and `allowedNestingDepth` ([296d0](https://github.com/JSQLParser/JSqlParser/commit/296d0321af58abc) manticore-projects) +- add fields holding the actual resolved table ([2500e](https://github.com/JSQLParser/JSqlParser/commit/2500e1ba72718a3) manticore-projects) +- `WITH ... AS NOT MATERIALIZED` ([2f6af](https://github.com/JSQLParser/JSqlParser/commit/2f6afbc3eb16d17) manticore-projects) +- SQL:2016 `BETWEEN [SYMMETRIC | ASYMMETRIC]` ([001ad](https://github.com/JSQLParser/JSqlParser/commit/001ad1c24c2a464) manticore-projects) +- Session Statement ([f0c09](https://github.com/JSQLParser/JSqlParser/commit/f0c098cec93656b) manticore-projects) +- Session Statement ([7d2e6](https://github.com/JSQLParser/JSqlParser/commit/7d2e6b65324ce57) manticore-projects) +- sync with Master ([e14d7](https://github.com/JSQLParser/JSqlParser/commit/e14d7eb1c4e9963) manticore-projects) +- JavaCC 8 keyword utils ([cfe2d](https://github.com/JSQLParser/JSqlParser/commit/cfe2d8ccaf7c76d) manticore-projects) +- Complete on JavaCC-8 ([1b7ed](https://github.com/JSQLParser/JSqlParser/commit/1b7ed2d7be000ce) manticore-projects) +- Optimise performance ([e91c4](https://github.com/JSQLParser/JSqlParser/commit/e91c480b0bbe0a9) manticore-projects) +- avoid looping through the tokens every single time ([b18fb](https://github.com/JSQLParser/JSqlParser/commit/b18fbca1f48e63e) manticore-projects) +- avoid looping through the tokens every single time ([7ac6c](https://github.com/JSQLParser/JSqlParser/commit/7ac6cd0fa08d713) manticore-projects) +- add proper JMH benchmarks ([21c98](https://github.com/JSQLParser/JSqlParser/commit/21c983fc1f4f3f2) manticore-projects) +- JavaCC-8 ([9d144](https://github.com/JSQLParser/JSqlParser/commit/9d1442e9a4800e2) manticore-projects) +- remove all semantic lookaheads ([5abca](https://github.com/JSQLParser/JSqlParser/commit/5abcaeaede27cfe) manticore-projects) ### Bug Fixes -- bring back `SYNTACTIC LOOKAHEAD` where it makes sense ([b3c5b](https://github.com/JSQLParser/JSqlParser/commit/b3c5b63344de193) manticore-projects) -- the Quotes Token manipulation ([bad81](https://github.com/JSQLParser/JSqlParser/commit/bad818e0b872c6a) manticore-projects) +- visit pipe expressions through shared deparser paths (#2618) ([537f3](https://github.com/JSQLParser/JSqlParser/commit/537f3a6dd1e35e7) Minjae Lee) +- span a binary condition's node from its left operand (#2615) ([0ecf0](https://github.com/JSQLParser/JSqlParser/commit/0ecf096d23acc1b) Knut Wannheden) +- preserve ordered table elements through legacy list edits (#2607) ([eb27c](https://github.com/JSQLParser/JSqlParser/commit/eb27c78bebccdb7) Minjae Lee) +- recognise set operation quantifiers written in any case (#2614) ([537c8](https://github.com/JSQLParser/JSqlParser/commit/537c8c1bee16907) Knut Wannheden) +- traverse and deparse structured table partitions (#2610) ([ef42d](https://github.com/JSQLParser/JSqlParser/commit/ef42d0b6ee91f65) Minjae Lee) +- share expression-aware table constraint rendering (#2609) ([9caed](https://github.com/JSQLParser/JSqlParser/commit/9caed4170713aed) Minjae Lee) +- preserve structured column options when editing specifications (#2608) ([fec4e](https://github.com/JSQLParser/JSqlParser/commit/fec4ec40e15ef53) Minjae Lee) +- keep CREATE TABLE option tokens in sync with typed options (#2606) ([d730b](https://github.com/JSQLParser/JSqlParser/commit/d730bf00fee62a0) Minjae Lee) +- **grammar** resolve JavaCC choice conflicts and gate the build against new ones ([8bb33](https://github.com/JSQLParser/JSqlParser/commit/8bb338572440cad) Andreas Reichel) +- **parser** preserve Oracle PRIOR position on all condition expressions (#2601) (#2602) ([37744](https://github.com/JSQLParser/JSqlParser/commit/3774451c3fa7f3e) fudian) +- **parser** parse Oracle outer join operator (+) after LIKE / SIMILAR TO operands (#2598) (#2600) ([35f84](https://github.com/JSQLParser/JSqlParser/commit/35f84d5a006c382) fudian) +- **parser** classify ODBC timestamp interval arguments (#2596) ([a7a5f](https://github.com/JSQLParser/JSqlParser/commit/a7a5fa5032447b2) Minjae Lee) +- **parser** resolve shadowed contextual keyword tokens (#2597) ([51e29](https://github.com/JSQLParser/JSqlParser/commit/51e29a042ce4a50) Minjae Lee) +- **parser** align empty statement input handling (#2594) ([1bef5](https://github.com/JSQLParser/JSqlParser/commit/1bef513f6f9fc7c) Minjae Lee) +- **parser** parse COMMENT ON ... IS NULL as comment removal (#2562) (#2563) ([b2115](https://github.com/JSQLParser/JSqlParser/commit/b2115ac8868dc8e) Paul_Kim) +- **parser** parse Oracle outer join operator (+) on BETWEEN operands (#672) (#2564) ([5bdd4](https://github.com/JSQLParser/JSqlParser/commit/5bdd4bf04ff884f) fudian) +- **parser** parse stacked NOT prefixes as nested NotExpression (#1170) (#2556) ([6c726](https://github.com/JSQLParser/JSqlParser/commit/6c726d8800ba1cc) fudian) +- **parser** parse INSERT ON CONFLICT after a join's trailing ON (#2551) ([ae37f](https://github.com/JSQLParser/JSqlParser/commit/ae37fd2aa06c324) fudian) +- **adapter** connect StatementVisitorAdapter sub-visitors and traverse DML CTEs ([1b57b](https://github.com/JSQLParser/JSqlParser/commit/1b57bfa41cef000) Andreas Reichel) +- **parser** support MySQL table rename variants (#2543) ([bb377](https://github.com/JSQLParser/JSqlParser/commit/bb377195dd622bd) Minjae Lee) +- **parser** model MySQL ALTER TABLE partition operations (#2540) ([80305](https://github.com/JSQLParser/JSqlParser/commit/80305c756fae362) Minjae Lee) +- **parser** model MySQL column type modifiers (#2537) ([bb2f2](https://github.com/JSQLParser/JSqlParser/commit/bb2f269e3b7489e) Minjae Lee) +- **parser** preserve unique index comments in ALTER TABLE (#2533) ([2243e](https://github.com/JSQLParser/JSqlParser/commit/2243e2ba78aa3d0) Minjae Lee) +- **parser** distinguish MySQL constraint and index names (#2534) ([bc9ca](https://github.com/JSQLParser/JSqlParser/commit/bc9ca85ecf1fd30) Minjae Lee) +- **parser** model PostgreSQL CREATE INDEX clauses (#2536) ([3ba25](https://github.com/JSQLParser/JSqlParser/commit/3ba25467ab9d14c) Minjae Lee) +- **parser** model PostgreSQL declarative partitions (#2535) ([c77bb](https://github.com/JSQLParser/JSqlParser/commit/c77bb0f0aa2c8d0) Minjae Lee) +- **parser** model ALTER COLUMN nullability actions (#2529) ([8397d](https://github.com/JSQLParser/JSqlParser/commit/8397d3a225e3fb3) Minjae Lee) +- **parser** allow VERSION in ALTER column clauses (#2530) ([a6da4](https://github.com/JSQLParser/JSqlParser/commit/a6da40051291b3c) Minjae Lee) +- memoize long-chain predicate walks behind a size threshold (#2520) ([33b03](https://github.com/JSQLParser/JSqlParser/commit/33b03e80b3bcd8e) fudian) +- prevent bare partition columns from inheriting the previous item's value (#2518) ([9a32f](https://github.com/JSQLParser/JSqlParser/commit/9a32ff568f58017) fudian) +- collect tables from all SELECT and DML trailing clauses in TablesNamesFinder (#2517) ([86418](https://github.com/JSQLParser/JSqlParser/commit/864181b74e42e8b) fudian) +- **parser** grammar ambiguities ([9c765](https://github.com/JSQLParser/JSqlParser/commit/9c76556d9b5910b) Andreas Reichel) +- **parser** parse qualified columns in GROUP BY (#2504) ([dff72](https://github.com/JSQLParser/JSqlParser/commit/dff722b593ed4e4) Chen Jianjin) +- **parser** support CAST to an array type for MySQL multi-valued indexes (#2506) ([e781b](https://github.com/JSQLParser/JSqlParser/commit/e781b3f27720347) Minjae Lee) +- **parser** support PostgreSQL # binary operator (bitwise XOR / geometric intersection) (#2507) ([d8d05](https://github.com/JSQLParser/JSqlParser/commit/d8d054aa6038a47) fudian) +- **parser** support MySQL index options and prefix-length key parts in index DDL (#2505) ([9fc38](https://github.com/JSQLParser/JSqlParser/commit/9fc38bd13bdaa7b) Minjae Lee) +- **parser** support leading CONTENT/DOCUMENT keyword in XMLPARSE (#2501) ([46584](https://github.com/JSQLParser/JSqlParser/commit/46584159b957706) Liming Deng) +- **parser** support CHARACTER SET in CAST CHAR types (#2497) ([1e4e9](https://github.com/JSQLParser/JSqlParser/commit/1e4e92bafdfd0fa) Liming Deng) +- **parser** support Oracle USING INDEX TABLESPACE in ALTER TABLE ADD CONSTRAINT (#2498) ([aa7df](https://github.com/JSQLParser/JSqlParser/commit/aa7df62608b6a88) Liming Deng) +- **parser** support IF EXISTS in ALTER TABLE MODIFY COLUMN (#2494) ([44c0f](https://github.com/JSQLParser/JSqlParser/commit/44c0fe50747fbe6) Liming Deng) +- support character set introduced string literals (#2493) ([158b5](https://github.com/JSQLParser/JSqlParser/commit/158b5800b4fd370) Liming Deng) +- support Oracle inline constraint states in CREATE TABLE (#2491) ([f5ecb](https://github.com/JSQLParser/JSqlParser/commit/f5ecba7dc51fab7) Liming Deng) +- keep all clauses when deparsing SELECT without FROM (#2483) (#2484) ([70139](https://github.com/JSQLParser/JSqlParser/commit/70139091d9d78f2) fudian) +- complete TablesNamesFinder traversal for piped queries, DML side clauses and analytic functions (#2478) (#2479) ([ad69e](https://github.com/JSQLParser/JSqlParser/commit/ad69eccc9928b92) fudian) +- reject illegal MERGE WHEN NOT MATCHED clause pairings (#2480) (#2481) ([a8cb4](https://github.com/JSQLParser/JSqlParser/commit/a8cb410ad118e78) fudian) +- treat a positional parameter '?' as a valid end of the ternary then-branch (#2477) ([f41c0](https://github.com/JSQLParser/JSqlParser/commit/f41c0b8aa29cd22) fudian) +- do not treat a named parameter ':' as the ternary else separator (#2476) ([5080d](https://github.com/JSQLParser/JSqlParser/commit/5080d19fab90463) fudian) +- allow GROUPS as identifier by making it a non-reserved keyword (#2473) (#2474) ([edfbb](https://github.com/JSQLParser/JSqlParser/commit/edfbb0ac050afa0) fudian) +- support ClickHouse ORDER BY WITH FILL and INTERPOLATE (#2467) (#2469) ([406a4](https://github.com/JSQLParser/JSqlParser/commit/406a4d41341d30f) fudian) +- support ClickHouse C-style ternary operator (? :) in expressions (#2436) (#2466) ([60b08](https://github.com/JSQLParser/JSqlParser/commit/60b0875abc1b2a5) fudian) +- allow scalar subquery as LIMIT row count (#2359) (#2464) ([2989c](https://github.com/JSQLParser/JSqlParser/commit/2989cb1d9c76774) fudian) +- support PostgreSQL composite row expansion (function()).* (#2463) ([7ced3](https://github.com/JSQLParser/JSqlParser/commit/7ced34d2bdee536) fudian) +- support SQL Server CREATE INDEX INCLUDE columns (#2462) ([b5adb](https://github.com/JSQLParser/JSqlParser/commit/b5adb60628ea504) Chen Jianjin) +- model interval qualifier as a structured property (#1728) (#2456) ([c9930](https://github.com/JSQLParser/JSqlParser/commit/c9930e36804d639) fudian) +- support PostgreSQL GROUPS window frames (#2460) ([d92f4](https://github.com/JSQLParser/JSqlParser/commit/d92f4074d3e1267) Chen Jianjin) +- **parser** keep parenthesized multi-param lambda as first function arg (#2455) ([b444b](https://github.com/JSQLParser/JSqlParser/commit/b444bc2a894dbdb) Ivan Clark) +- allow UNIQUE INDEX in CREATE TABLE (#2457) ([32a79](https://github.com/JSQLParser/JSqlParser/commit/32a79495a752f06) fudian) +- support ClickHouse tuple positional access (#2454) ([5bec7](https://github.com/JSQLParser/JSqlParser/commit/5bec712642fd267) Chen Jianjin) +- stop 0x hex literal from swallowing trailing whitespace (#2435) (#2450) ([18b12](https://github.com/JSQLParser/JSqlParser/commit/18b12f1864de80a) fudian) +- allow arbitrary column aliases in LATERAL VIEW (#2452) ([f92f5](https://github.com/JSQLParser/JSqlParser/commit/f92f5e5afd7d8b0) fudian) +- accept VISIBLE/INVISIBLE index and column attributes in CREATE TABLE and CREATE INDEX (#2449) ([aa542](https://github.com/JSQLParser/JSqlParser/commit/aa542c0822e8d00) Armand Parajon) +- support nested parametric data types as CAST target (#2451) ([3997b](https://github.com/JSQLParser/JSqlParser/commit/3997ba4347981e7) fudian) +- add 'K_TYPE' to KeywordOrIdentifier to allow 'type' as a column name (#2448) ([f0ba1](https://github.com/JSQLParser/JSqlParser/commit/f0ba1235028f37a) Minjae Lee) +- **parser** consume nested block comments in a MORE action ([83d7b](https://github.com/JSQLParser/JSqlParser/commit/83d7b5ac46b812c) Andreas Reichel) +- **test** resolve classes dir via CodeSource URI to unbreak Windows ([49622](https://github.com/JSQLParser/JSqlParser/commit/496221a653af093) Andreas Reichel) +- **grammar** use `TokenMgrException` instead of old `TokenMgrError` ([850fb](https://github.com/JSQLParser/JSqlParser/commit/850fb70d08de3c7) Andreas Reichel) +- **grammar** parse bracketed operands in function arguments and expression lists ([ca2b3](https://github.com/JSQLParser/JSqlParser/commit/ca2b3d71a870385) Andreas Reichel) +- split CCJSqlParserTokenManager static initializers to avoid 64KB method limit (#2425) ([cf5bb](https://github.com/JSQLParser/JSqlParser/commit/cf5bbc9a6216315) Hayssam Saleh) +- exponential backtracking on deeply nested bracketed expressions ([fff8a](https://github.com/JSQLParser/JSqlParser/commit/fff8a081e43d9b5) manticore-projects) +- **parser** pass modifier to ExceptOp/MinusOp constructors (#2419) (#2420) ([7fc30](https://github.com/JSQLParser/JSqlParser/commit/7fc300f7b0968ec) Alex Towell) +- **parser** parse mysql fulltext AGAINST concat expression (#2413) ([5788c](https://github.com/JSQLParser/JSqlParser/commit/5788ca067abf76a) Liming Deng) +- **parser** support KEY-prefixed expressions like KEY chain.entity (#2409) ([bfcb8](https://github.com/JSQLParser/JSqlParser/commit/bfcb8b752539ab3) Liming Deng) +- reduce timeout since we are too fast now :-D ([126c1](https://github.com/JSQLParser/JSqlParser/commit/126c1a16abaab2f) manticore-projects) +- improve performance by refactoring `Condition`, `RegularCondition`, `AndExpression` ([6c5d7](https://github.com/JSQLParser/JSqlParser/commit/6c5d7ef60351a49) manticore-projects) +- restore GENERATED ALWAYS AS IDENTITY parsing (#2397) ([bd3ce](https://github.com/JSQLParser/JSqlParser/commit/bd3ce05f356dac2) Liming Deng) +- **parser** resolve grammar LOOKAHEAD conflicts (#2399) ([59dfc](https://github.com/JSQLParser/JSqlParser/commit/59dfc3b0e7167eb) Liming Deng) +- **clickhouse** support SELECT ... SETTINGS (#2395) ([a34db](https://github.com/JSQLParser/JSqlParser/commit/a34db0ce68fe6ac) Liming Deng) +- support ClickHouse parametric aggregate calls (#2393) ([64542](https://github.com/JSQLParser/JSqlParser/commit/64542c863a1325d) Liming Deng) +- support SQL Server ORDER BY ... FOR XML PATH parsing in subqueries (#2392) ([9de70](https://github.com/JSQLParser/JSqlParser/commit/9de70747b94e575) Liming Deng) +- ALTER TABLE with a USING INDEX clause (#2384) ([c7b3b](https://github.com/JSQLParser/JSqlParser/commit/c7b3bdbd00cf96e) Liming Deng) +- remove obsolete Grammar option ([93515](https://github.com/JSQLParser/JSqlParser/commit/935151493cc9db9) manticore-projects) +- module-info.java ([484ea](https://github.com/JSQLParser/JSqlParser/commit/484eaa1c0f623cc) Andreas Reichel) +- DuckDB `STRUCT` ([297ef](https://github.com/JSQLParser/JSqlParser/commit/297ef84673521fd) Andreas Reichel) +- parsing `STRUCT` data types ([aaebe](https://github.com/JSQLParser/JSqlParser/commit/aaebe591001a164) Andreas Reichel) +- add some LOOKAHEAD ([2d739](https://github.com/JSQLParser/JSqlParser/commit/2d7393499e4d987) Andreas Reichel) +- avoid visiting twice ([49958](https://github.com/JSQLParser/JSqlParser/commit/49958b6ba96898f) Andreas Reichel) +- avoid NPE and expose `modifier` ([eeb04](https://github.com/JSQLParser/JSqlParser/commit/eeb04004da797c6) Andreas Reichel) +- return Dollar quoted text as `StringValue` ([95ebd](https://github.com/JSQLParser/JSqlParser/commit/95ebda5a6112cd7) manticore-projects) +- avoid NPE ([12600](https://github.com/JSQLParser/JSqlParser/commit/12600fb41957205) manticore-projects) +- possible NPE ([915aa](https://github.com/JSQLParser/JSqlParser/commit/915aa35d696b70b) manticore-projects) +- possible NPE ([dd05e](https://github.com/JSQLParser/JSqlParser/commit/dd05e11ae6dbe89) manticore-projects) +- `FromItemVisitorAdaptor` also needs a `SelectVisitor` ([178ca](https://github.com/JSQLParser/JSqlParser/commit/178ca057be78c00) manticore-projects) +- resolved physical tables must be clones, not references ([96321](https://github.com/JSQLParser/JSqlParser/commit/963217f48732426) manticore-projects) +- GroupByElement::getGroupByExpressionList shall not return raw ExpressionList ([f14c6](https://github.com/JSQLParser/JSqlParser/commit/f14c6115bd3e715) manticore-projects) +- increase Max. Nesting Depth to 16 (from 10) ([b8c0e](https://github.com/JSQLParser/JSqlParser/commit/b8c0ed58f3ee572) manticore-projects) +- date/time functions colliding with implicit casts ([6e91c](https://github.com/JSQLParser/JSqlParser/commit/6e91c3e1e3c94b3) manticore-projects) +- precedence of the `InExpression` ([0f9e4](https://github.com/JSQLParser/JSqlParser/commit/0f9e477944332c4) manticore-projects) +- revert to Semantic LOOKAHEAD for `SubSelect` in `PrimaryExpression` ([7f068](https://github.com/JSQLParser/JSqlParser/commit/7f068f6bd09623e) manticore-projects) +- bring back `SYNTACTIC LOOKAHEAD` where it makes sense ([b3c5b](https://github.com/JSQLParser/JSqlParser/commit/b3c5b63344de193) manticore-projects) +- the Quotes Token manipulation ([bad81](https://github.com/JSQLParser/JSqlParser/commit/bad818e0b872c6a) manticore-projects) ### Other changes +**Clarify PostgreSQL DO support and verify the reported block (#2633)** + +* Clarify PostgreSQL DO support and verify the reported block +* Keep DO round-trip assertions independent of fixture license comments + +[02b9d](https://github.com/JSQLParser/JSqlParser/commit/02b9d94072196b9) Minjae Lee *2026-09-13 12:10:51* + +**Render DML source clauses through shared visitor paths (#2630)** + + +[4b88a](https://github.com/JSQLParser/JSqlParser/commit/4b88a445419c846) Minjae Lee *2026-09-13 10:11:39* + +**Fix benchmark configuration and isolated parser class loading (#2629)** + + +[95140](https://github.com/JSQLParser/JSqlParser/commit/95140c895d836c5) Minjae Lee *2026-09-13 10:10:03* + +**Keep ALTER key accessors connected to structured index state (#2628)** + +* Keep ALTER key accessors connected to structured index state +* Clarify ALTER key element type for static analysis +* Make remaining ALTER key element types explicit +* Simplify ALTER key adapters and document replacement semantics + +[7739c](https://github.com/JSQLParser/JSqlParser/commit/7739c08181110e8) Minjae Lee *2026-09-13 10:07:36* + +**Protect shared dialect presets from external mutation (#2627)** + + +[b3b20](https://github.com/JSQLParser/JSqlParser/commit/b3b208489a19e7c) Minjae Lee *2026-09-13 10:03:57* + +**Share parser execution and preserve cancellation signals (#2626)** + + +[35105](https://github.com/JSQLParser/JSqlParser/commit/35105a0e18b2d9f) Minjae Lee *2026-09-13 09:59:42* + +**Reuse common JOIN traversal when discovering UPDATE tables (#2625)** + + +[a2baa](https://github.com/JSQLParser/JSqlParser/commit/a2baa18d7ce1589) Minjae Lee *2026-09-13 09:55:54* + +**Render DML RETURNING expressions through shared visitor handling (#2624)** + + +[38224](https://github.com/JSQLParser/JSqlParser/commit/3822466957f69f9) Minjae Lee *2026-09-13 09:54:53* + +**Validate every UPDATE assignment through shared UpdateSet traversal (#2623)** + + +[c314d](https://github.com/JSQLParser/JSqlParser/commit/c314df2b32c6fac) Minjae Lee *2026-09-13 09:53:02* + +**Structure FOR PORTION intervals in UPDATE and DELETE (#2622)** + + +[33c40](https://github.com/JSQLParser/JSqlParser/commit/33c4030433a3df2) Minjae Lee *2026-09-13 09:51:36* + +**Preserve ASOF joins through shared join rendering (#2621)** + + +[68608](https://github.com/JSQLParser/JSqlParser/commit/6860852112d6403) Minjae Lee *2026-09-13 09:49:48* + +**Support structured XMLFOREST arguments with aliases (#2619)** + + +[73bd3](https://github.com/JSQLParser/JSqlParser/commit/73bd3c498eed103) Minjae Lee *2026-09-13 09:41:15* + +**Support SQL Server SET IDENTITY_INSERT with a structured table target (#2605)** + + +[894ab](https://github.com/JSQLParser/JSqlParser/commit/894ab4d7e03717b) Minjae Lee *2026-09-12 00:35:38* + +**Support SQL Server boolean SET options with shared statement rendering (#2604)** + +* Co-authored-by: manticore-projects <andreas@manticore-projects.com> + +[f473c](https://github.com/JSQLParser/JSqlParser/commit/f473ccfd5f032c1) Minjae Lee *2026-09-12 00:34:26* + +**Support SQL Server INSERT BULK declarations (#2591)** + + +[bf2c9](https://github.com/JSQLParser/JSqlParser/commit/bf2c9ac773e8521) Minjae Lee *2026-09-11 21:41:04* + +**Support SQL Server XML methods on subquery results (#2590)** + +* Co-authored-by: manticore-projects <andreas@manticore-projects.com> + +[15156](https://github.com/JSQLParser/JSqlParser/commit/1515637c08a461a) Minjae Lee *2026-09-11 16:15:55* + +**Support PostgreSQL DO wrappers with preserved routine body literals (#2589)** + +* Co-authored-by: manticore-projects <andreas@manticore-projects.com> + +[1bc94](https://github.com/JSQLParser/JSqlParser/commit/1bc942422801523) Minjae Lee *2026-09-11 16:12:59* + +**Accept SQL Server CREATE TABLE trailing commas with shared element parsing (#2603)** + + +[179ee](https://github.com/JSQLParser/JSqlParser/commit/179ee11e17780a4) Minjae Lee *2026-09-11 16:10:39* + +**Split PostgreSQL hash operators from adjacent identifiers (#2588)** + + +[a946e](https://github.com/JSQLParser/JSqlParser/commit/a946e7715dec34d) Minjae Lee *2026-09-11 16:05:53* + +**Support Teradata UPDATE FROM before SET with shared clause rendering (#2587)** + +* Co-authored-by: mj <mj@mjui-MacBookPro.local> + +[a206b](https://github.com/JSQLParser/JSqlParser/commit/a206b861da4ff68) Minjae Lee *2026-09-11 15:20:17* + +**Structure SQL Server routine operations and table return types (#2593)** + +* Co-authored-by: mj <mj@mjui-MacBookPro.local> + +[d18de](https://github.com/JSQLParser/JSqlParser/commit/d18de825e152d05) Minjae Lee *2026-09-11 14:50:16* + +**Support CockroachDB ALTER PRIMARY KEY with structured hash options (#2585)** + +* Co-authored-by: mj <mj@mjui-MacBookPro.local> + +[7c6a9](https://github.com/JSQLParser/JSqlParser/commit/7c6a9fcbf5bd7bd) Minjae Lee *2026-09-11 14:47:57* + +**Support Doris distribution hints and share join keyword rendering (#2584)** + + +[78cff](https://github.com/JSQLParser/JSqlParser/commit/78cff6b34be54ca) Minjae Lee *2026-09-11 09:47:52* + +**Support Oracle anonymous blocks and implicit procedure calls (#2592)** + + +[17f03](https://github.com/JSQLParser/JSqlParser/commit/17f033df04e3b10) Minjae Lee *2026-09-11 09:08:42* + +**Support dialect-specific CREATE INDEX definitions (#2586)** + + +[4be19](https://github.com/JSQLParser/JSqlParser/commit/4be195b1e44d9b0) Minjae Lee *2026-09-11 09:06:37* + +**Support MySQL SELECT INTO user variables with explicit dialect (#2583)** + + +[588bf](https://github.com/JSQLParser/JSqlParser/commit/588bf070e9341e2) Minjae Lee *2026-09-11 09:05:22* + +**Support recursive CTE CYCLE clauses with shared recursive clause rendering (#2566)** + + +[eddb1](https://github.com/JSQLParser/JSqlParser/commit/eddb1fb5bb5f6ef) Minjae Lee *2026-09-11 04:37:17* + +**Support SQL Server constraint clustering with explicit dialect (#2582)** + + +[c1a61](https://github.com/JSQLParser/JSqlParser/commit/c1a611db8aa3f93) Minjae Lee *2026-09-11 04:30:55* + +**Support Informix trailing constraint names and share constraint rendering (#2570)** + +* Support Informix trailing constraint names and share constraint rendering +* fix(parser): gate Informix constraints on dialect selection + +[329ee](https://github.com/JSQLParser/JSqlParser/commit/329ee6b6465aa7e) Minjae Lee *2026-09-11 03:26:45* + +**Support PostgreSQL tagged dollar strings and preserve literal bodies (#2572)** + +* Support PostgreSQL tagged dollar strings and preserve literal bodies +* fix(parser): enable tagged dollar strings through PostgreSQL dialect + +[8ff4f](https://github.com/JSQLParser/JSqlParser/commit/8ff4f6488c7cf60) Minjae Lee *2026-09-11 03:12:26* + +**Support ksqlDB window grace periods and EMIT FINAL (#2571)** + +* Support ksqlDB window grace periods and EMIT FINAL +* Group ksqlDB window support with queries in the support table + +[e8479](https://github.com/JSQLParser/JSqlParser/commit/e8479ca3d93c6c8) Minjae Lee *2026-09-11 02:57:08* + +**Complete MySQL index options and share CREATE INDEX rendering (#2567)** + +* Complete MySQL index algorithm and lock options and share CREATE INDEX rendering +* Split shared index rendering into focused helpers +* Compare median monotonic durations in bracket growth regression checks + +[381d1](https://github.com/JSQLParser/JSqlParser/commit/381d1a9de236bd1) Minjae Lee *2026-09-11 02:48:19* + +**Support PostgreSQL conflict index elements, OVERRIDING USER and MERGE RETURNING (#2569)** + +* Support PostgreSQL conflict index elements, OVERRIDING USER and MERGE RETURNING +* Preserve bulk additions through the conflict column view + +[ba4dc](https://github.com/JSQLParser/JSqlParser/commit/ba4dce9d26016f2) Minjae Lee *2026-09-11 02:46:04* + +**Preserve EXEC bind arguments and support OUTPUT parameters (#2580)** + + +[2de24](https://github.com/JSQLParser/JSqlParser/commit/2de24179541d6c8) Minjae Lee *2026-09-11 02:44:11* + +**Support SQL Server table variables through shared table references (#2581)** + + +[7bb6e](https://github.com/JSQLParser/JSqlParser/commit/7bb6e1c4440f1fa) Minjae Lee *2026-09-11 02:41:51* + +**Fix parseStatements failure propagation and executor cleanup (#2568)** + + +[d9109](https://github.com/JSQLParser/JSqlParser/commit/d9109134703cb11) Minjae Lee *2026-09-11 02:40:33* + +**Validate CTE bodies through the existing statement dispatch (#2578)** + + +[72521](https://github.com/JSQLParser/JSqlParser/commit/7252121652154c6) Minjae Lee *2026-09-11 02:30:36* + +**Analyse analytic and conversion functions with complete window traversal (#2579)** + + +[01d7c](https://github.com/JSQLParser/JSqlParser/commit/01d7c3eea2d5e98) Minjae Lee *2026-09-11 02:27:12* + +**Fix DISTINCT traversal and share optional select-item visits (#2577)** + +* Visit optional DISTINCT ON items without changing the AST +* Verify plain DISTINCT retains unproven function analysis + +[db61f](https://github.com/JSQLParser/JSqlParser/commit/db61f4bc04a7613) Minjae Lee *2026-09-11 02:24:16* + +**Support structured PostgreSQL role, privilege and trigger DDL (#2555)** + +* Support structured PostgreSQL role privilege and trigger DDL +* Align access DDL field layout and simplify trigger rendering + +[01768](https://github.com/JSQLParser/JSqlParser/commit/01768813eb60295) Minjae Lee *2026-09-10 09:50:05* + +**Support structured PostgreSQL publication and subscription DDL (#2554)** + +* Support structured PostgreSQL logical replication DDL +* Exclude PostgreSQL fixtures from Transact-SQL linting + +[bc18f](https://github.com/JSQLParser/JSqlParser/commit/bc18fdf99b52b4b) Minjae Lee *2026-09-10 09:39:51* + +**Support SQL Server ALTER TABLE ADD DEFAULT constraints (#2561)** + + +[d45f7](https://github.com/JSQLParser/JSqlParser/commit/d45f79699d43bc0) Minjae Lee *2026-09-10 08:34:43* + +**Support Oracle XMLSERIALIZE CONTENT and DOCUMENT forms (#2560)** + + +[8ecff](https://github.com/JSQLParser/JSqlParser/commit/8ecff15fecbc8bf) Minjae Lee *2026-09-10 08:32:34* + +**Support SQL Server typed XML schema collections (#2559)** + +* Support SQL Server typed XML schema collections +* Keep XML type lookahead independent of EXPLAIN additions + +[b2c84](https://github.com/JSQLParser/JSqlParser/commit/b2c84f98e6992a6) Minjae Lee *2026-09-10 08:31:04* + +**Support Redshift materialized view BACKUP clauses (#2557)** + + +[b5595](https://github.com/JSQLParser/JSqlParser/commit/b55956b649a06f5) Minjae Lee *2026-09-10 08:30:02* + +**Support PostgreSQL parenthesized EXPLAIN options (#2558)** + + +[4c8f5](https://github.com/JSQLParser/JSqlParser/commit/4c8f593e73948df) Minjae Lee *2026-09-10 08:29:01* + +**Model MySQL DROP object lists (#2545)** + +* feat(parser): model MySQL DROP object lists +* test: keep DROP assertions Java 11 compatible + +[5c59b](https://github.com/JSQLParser/JSqlParser/commit/5c59b7bc24a1af8) Minjae Lee *2026-09-05 00:15:16* + +**Model MySQL ANALYZE TABLE statements (#2542)** + +* feat(parser): model MySQL ANALYZE TABLE +* test: keep ANALYZE assertions Java 11 compatible + +[53200](https://github.com/JSQLParser/JSqlParser/commit/5320066eb287807) Minjae Lee *2026-09-05 00:12:03* + +**Support structured MySQL table definitions (#2541)** + +* refactor(ast): model structured table definitions +* fix(parser): unify MySQL table definitions +* fix(parser): model common MySQL table options +* fix(ast): initialize check constraint kind + +[a6450](https://github.com/JSQLParser/JSqlParser/commit/a64505266ffce63) Minjae Lee *2026-09-05 00:10:27* + +**corrected missing licence headers** + + +[495da](https://github.com/JSQLParser/JSqlParser/commit/495dafec9e37e22) tw *2026-09-04 22:12:41* + +**Update ci.yml** + + +[4b89a](https://github.com/JSQLParser/JSqlParser/commit/4b89a35da568df5) manticore-projects *2026-09-04 07:24:50* + +**Create requirements-docs.txt** + + +[fd59b](https://github.com/JSQLParser/JSqlParser/commit/fd59b5267ed77f7) manticore-projects *2026-09-04 07:24:28* + +**Update ci.yml** + +* Fix workflow + +[8186f](https://github.com/JSQLParser/JSqlParser/commit/8186f38e3d8b345) manticore-projects *2026-09-04 07:10:25* + +**Support MySQL CHECK constraint enforcement (#2528)** + + +[125de](https://github.com/JSQLParser/JSqlParser/commit/125de36c05b6dd7) Minjae Lee *2026-09-03 06:40:12* + +**Support PostgreSQL CREATE INDEX header options (#2527)** + + +[43314](https://github.com/JSQLParser/JSqlParser/commit/433143e4aaacfb5) Minjae Lee *2026-09-03 06:39:21* + +**Add MySQL CREATE TABLE partition parsing (#2521)** + + +[536fe](https://github.com/JSQLParser/JSqlParser/commit/536fe3b62de6e43) Minjae Lee *2026-09-03 06:38:22* + +**anchor oracle hint pattern to avoid quadratic backtracking (#2495)** + + +[a5765](https://github.com/JSQLParser/JSqlParser/commit/a5765f3f905d41a) Javid Khan *2026-08-22 12:45:11* + +**avoid quadratic backtracking in Server name pattern (#2489)** + + +[edd5b](https://github.com/JSQLParser/JSqlParser/commit/edd5bfc1a513f01) Javid Khan *2026-08-22 02:41:56* + +**Separate JSON operators with spaces so #> deparses correctly (#2487)** + +* JsonExpression.toString glued each operator directly onto its operand, so +* a #> ... deparsed as a#>... . Because # is a legal identifier character, that +* re-parses as a# > ... -- a GreaterThan, a different tree from the original +* JsonExpression. Emit a space on each side of the operator, matching the +* JsonOperator visitor. + +[55b23](https://github.com/JSQLParser/JSqlParser/commit/55b23637604ea24) Denis Gregor *2026-08-19 03:25:49* + +**Update changelog** + + +[fa731](https://github.com/JSQLParser/JSqlParser/commit/fa731f9b1b9d5fd) Andreas Reichel *2026-08-18 01:18:54* + +**Update Gradle wrapper** + + +[37e17](https://github.com/JSQLParser/JSqlParser/commit/37e17a09a0b7cfd) Andreas Reichel *2026-08-17 01:32:56* + +**Update changelog** + + +[0944d](https://github.com/JSQLParser/JSqlParser/commit/0944d553e5b7fde) Andreas Reichel *2026-08-13 14:31:54* + +**Add support PostgreSQL ROWS FROM table functions (#2416)** + + +[14db8](https://github.com/JSQLParser/JSqlParser/commit/14db886f5a23e56) Liming Deng *2026-07-30 03:07:55* + +**use Locale.ROOT for case-insensitive keyword normalisation (#2437)** + +* Signed-off-by: dxbjavid <dxbjavid@gmail.com> + +[82b7c](https://github.com/JSQLParser/JSqlParser/commit/82b7c26334df876) Javid Khan *2026-07-30 03:06:47* + +**[Fix] #2443 - NPE In TableNamesFinder with AnalyticExpression (#2444)** + +* [Fix] #2443 - NPE In TableNamesFinder with AnalyticExpression +* If you attempt to tree walk an AnalyticExpression in TableNamesFinder with an SQL that has a window function without a range and without an offset, such as `SELECT c, SUM(COUNT(*)) OVER (ORDER BY c ASC ROWS UNBOUNDED PRECEDING) FROM tbl GROUP BY c`, an NPE is thrown. This adds defensive checks around the range and offset expressions being null. +* More aggressive NPE protection +* --------- +* Co-authored-by: David Hayes <dhayes@adobe.com> + +[56468](https://github.com/JSQLParser/JSqlParser/commit/5646842a0b1c37a) David Hayes *2026-07-25 02:34:37* + +**[feat] Support for JSON_TABLE (#2328)** + +* [feat] Initial support for JSON_TABLE +* [feat] Additional support for JSON_TABLE +* [chore] Code Review +* [chore] Started to add oracle JSON_TABLE features +* [feat] Fixed onError for Oracle, added onEmptyClause, addedParsingType, formatJson for Body +* [feat] Added EXISTS keyword +* [chore] spotless +* [chore] cleanup +* [bugfix] Fixed token limit again... +* [chore] One more test, code cleanup + +[c5e2f](https://github.com/JSQLParser/JSqlParser/commit/c5e2fdcd40e81fa) Andreas Neumann *2026-04-02 16:42:55* + +**Add support MySQL SELECT INTO OUTFILE/DUMPFILE (#2418)** + +* Add support MySQL SELECT INTO OUTFILE/DUMPFILE +* fix + +[c0e1d](https://github.com/JSQLParser/JSqlParser/commit/c0e1d0528a9b114) Liming Deng *2026-03-22 07:31:33* + +**Fix dollar-quoted CREATE FUNCTION statement splitting (#2410)** + +* Fix dollar-quoted CREATE FUNCTION statement splitting +* Fix exception + +[c5b85](https://github.com/JSQLParser/JSqlParser/commit/c5b85abffdb00ae) Liming Deng *2026-03-14 18:09:38* + +**Add support MySQL functional index key parts in ALTER/CREATE INDEX (#2405)** + + +[7b87d](https://github.com/JSQLParser/JSqlParser/commit/7b87d0816dea41e) Liming Deng *2026-03-08 15:53:07* + +**Fix nested PostgreSQL composite field access after cast (#2404)** + + +[5b5fe](https://github.com/JSQLParser/JSqlParser/commit/5b5fe6c2a7e2908) Liming Deng *2026-03-08 15:50:58* + +**Add support parse SEARCH BREADTH/DEPTH FIRST in recursive CTEs (#2402)** + +* fix(parser): parse SEARCH BREADTH/DEPTH FIRST in recursive CTEs +* fix + +[1ee36](https://github.com/JSQLParser/JSqlParser/commit/1ee36829fbce7ef) Liming Deng *2026-03-08 08:31:28* + +**Update README.md to remove General SQL Parser mention** + +* Removed a comparison of General SQL Parser from alternatives section. + +[c3b15](https://github.com/JSQLParser/JSqlParser/commit/c3b1531150b0a03) Tobias *2026-03-07 21:12:26* + +**Add support CREATE SEQUENCE AS data type (#2400)** + +* Add support CREATE SEQUENCE AS data type +* format cod + +[22da3](https://github.com/JSQLParser/JSqlParser/commit/22da32654622925) Liming Deng *2026-03-06 16:09:41* + +**Add support MySQL named UNIQUE index in ALTER TABLE (#2391)** + + +[fb36c](https://github.com/JSQLParser/JSqlParser/commit/fb36cc6a2ec46ba) Liming Deng *2026-03-01 14:58:01* + +**Add support SELECT SAMPLE 0.1/OFFSET and CREATE TABLE SAMPLE BY (#2390)** + +* fix(clickhouse): support SELECT SAMPLE 0.1/OFFSET and CREATE TABLE SAMPLE BY +* fix(clickhouse): support SELECT SAMPLE 0.1/OFFSET and CREATE TABLE SAMPLE BY + +[e1620](https://github.com/JSQLParser/JSqlParser/commit/e16202441dd91c1) Liming Deng *2026-03-01 10:31:28* + +**Fix broken maven central svg url (#2389)** + + +[ecaa2](https://github.com/JSQLParser/JSqlParser/commit/ecaa26d3e0e1bbd) Liming Deng *2026-02-28 08:53:26* + +**Add support MySQL SPATIAL KEY (#2388)** + + +[a019a](https://github.com/JSQLParser/JSqlParser/commit/a019aa01085f110) Liming Deng *2026-02-25 16:39:25* + +**Fix alter table index descending (#2387)** + + +[763e9](https://github.com/JSQLParser/JSqlParser/commit/763e92d71d728de) Liming Deng *2026-02-24 13:00:47* + +**Add support for multiple Trino JSON functions (#2382)** + +* Adding support for clauses in JSON Functions. Fixes #2368 +* Adds support for the extra clauses found in Trino's JSON functions https://trino.io/docs/current/functions/json.html#json-exists, and includes test cases for these extra functions directly taken from the documentations. +* Before: +* Result "net.sf.jsqlparser.benchmark.JSQLParserBenchmark.parseSQLStatements": +* 34.858 ±(99.9%) 1.724 ms/op [Average] +* (min, avg, max) = (32.578, 34.858, 38.383), stdev = 2.302 +* CI (99.9%): [33.133, 36.582] (assumes normal distribution) +* After: +* Result "net.sf.jsqlparser.benchmark.JSQLParserBenchmark.parseSQLStatements": +* 36.154 ±(99.9%) 1.701 ms/op [Average] +* (min, avg, max) = (33.100, 36.154, 38.353), stdev = 2.271 +* CI (99.9%): [34.453, 37.855] (assumes normal distribution) +* Co-authored-by: David Hayes <dhayes@adobe.com> + +[d33d6](https://github.com/JSQLParser/JSqlParser/commit/d33d61a882d17ef) David Hayes *2026-02-24 12:23:52* + +**Fix oracle outer join case with nvl/coalesce (#2386)** + + +[834af](https://github.com/JSQLParser/JSqlParser/commit/834afe188e7ff46) Liming Deng *2026-02-24 12:15:14* + +**Add support clickhouse GLOBAL ANY/ALL JOIN syntax variants (#2385)** + + +[865a3](https://github.com/JSQLParser/JSqlParser/commit/865a3bf00d7a58d) Liming Deng *2026-02-23 13:38:22* + +**Add support for insert as row_alias (#2383)** + +* Add support for insert as row_alias +* format code + +[4e3d7](https://github.com/JSQLParser/JSqlParser/commit/4e3d7a56fdd4460) Liming Deng *2026-02-22 16:16:52* + +**[feat] Support for the legacy Postgres named parameter (#2374)** + +* feat: add postgresql named parameters support +* modify: PostgresNamedFunctionParameterTest.java +* refactor: code formatting +* --------- +* Co-authored-by: youngjoonkim <youngjoonkim@seowoninfo.com> + +[7c52e](https://github.com/JSQLParser/JSqlParser/commit/7c52e7fe8df21a2) youngjoon *2026-02-15 09:09:33* + +**Add support for ClickHouse PREWHERE clause (#2378)** + + +[8453c](https://github.com/JSQLParser/JSqlParser/commit/8453ca0fe7ce1a0) Liming Deng *2026-02-15 09:08:55* + +**Add support for ClickHouse MATERIALIZED column in CREATE TABLE (#2377)** + +* - allow `MATERIALIZED` in column specs +* - allow `ORDER BY` in table options +* - add regression test for `ENGINE = MergeTree() ORDER BY tuple()` + +[ededd](https://github.com/JSQLParser/JSqlParser/commit/ededd864b81b33a) Liming Deng *2026-02-13 17:12:48* + +**Delete .github/dependabot.yml** + + +[38c96](https://github.com/JSQLParser/JSqlParser/commit/38c963d6464eef4) manticore-projects *2026-02-12 08:26:46* + +**Add support for JOIN FETCH (#2375)** + + +[091ef](https://github.com/JSQLParser/JSqlParser/commit/091ef964eaa0822) Liming Deng *2026-02-12 08:06:12* + +**Support PostgreSQL-specific [CREATE SEQUENCE name START n ...] (#2348)** + +* Support PostgreSQL-specific [CREATE SEQUENCE name START n ...] +* I.e. no `START WITH` +* Fixes #2347 +* Support PostgreSQL-specific [CREATE SEQUENCE … INCREMENT n ...] +* I.e. no `INCREMENT BY` +* Fixes #2347 + +[40ccf](https://github.com/JSQLParser/JSqlParser/commit/40ccf4b87171494) mjog *2025-11-21 07:21:30* + +**[feat] Add 'K_DATA' to KeywordOrIdentifier to allow usages of 'data' as an identifier (#2340)** + +* Allows for support of data as a column name when changing or dropping a column in an alter table statement + +[2d83c](https://github.com/JSQLParser/JSqlParser/commit/2d83cea9ba65a7c) Will Needham *2025-11-21 02:18:54* + +**[fix] Enable qualified table name for DROP INDEX command (#2344)** + +* Thank you for your contribution. + +[8d967](https://github.com/JSQLParser/JSqlParser/commit/8d967803c780f5f) Ton Huisman *2025-11-21 02:17:13* + +**Update README.md (#2331)** + + +[01034](https://github.com/JSQLParser/JSqlParser/commit/01034cd08c3e9d7) Hayssam Saleh *2025-10-28 11:48:22* + +**[feat/refactor] AllColumns and AllTableColumns-Support for JSON_OBJECT (#2323)** + +* [feat] JSON_OBJECT support for AllColumns and AllTableColumns +* [feat] JSON_OBJECT support for AllColumns and AllTableColumns +* [feat] JSON_OBJECT support for AllColumns and AllTableColumns +* [feat] Split the parser syntax for JsonFunction, added more Tests, added Feature to disable Commas as key value separators +* [feat] Disable Expression as JSON_OBJECT key value via feature flag +* [chore] spotlessApply +* [feat] Use append method from JsonKeyValuePair + +[e8d60](https://github.com/JSQLParser/JSqlParser/commit/e8d605854ae9148) Andreas Neumann *2025-10-12 11:06:19* + +**[feat] Support for FOR READ ONLY/FOR FETCH ONLY (#2325)** + +* [feat] Support for FOR READ ONLY/FOR FETCH ONLY +* [chore] Spotless + +[e4004](https://github.com/JSQLParser/JSqlParser/commit/e400444402c7a6e) Andreas Neumann *2025-10-10 11:01:28* + +**[feat] Support for LOCK-Statements (#2321)** + +* Add support for LOCK-Statements +* Fixed comments +* ran updateKeywords +* Added test +* Checkstyle & Spotless +* Adjusted NOWAIT and WAIT-Setter + +[6697c](https://github.com/JSQLParser/JSqlParser/commit/6697c063493cac2) Andreas Neumann *2025-10-07 23:32:53* + +**Added new features to support the complete delete using syntax of pg database, such as subqueries, etc. (#2316)** + +* Co-authored-by: zhangkenan <zhangkenan@hexadb.com> + +[15798](https://github.com/JSQLParser/JSqlParser/commit/157988d1c5f6816) ConanZhang *2025-09-24 02:06:46* + +**Fix[2312] - Fixes issue with array in function declaration (#2313)** + +* Fix[2312] - Fixes issue with array<double> in function declaration +* ```sql +* WITH +* FUNCTION takesArray(x array<double>) +* RETURNS double +* RETURN x[1] + x[2] + x[3] +* SELECT takesArray(array[1.0, 2.0, 3.0]); +* ``` +* Is unable to be parsed as we're not able to capture the array<double> correctly. This PR fixes that, as well as some missing visitor changes from my previous PR. +* Fix spotlessJavaCheck +* --------- +* Co-authored-by: David Hayes <dhayes@adobe.com> + +[528dd](https://github.com/JSQLParser/JSqlParser/commit/528dd7227401d07) David Hayes *2025-09-17 01:20:36* + +**Fix[2306] - Adds support for Trino UDF (#2307)** + +* Fix[2306] - Adds support for Trino UDF +* Trino SQL allows you to specify User Defined functions in the WITH clause ahead of a statement. This adds support for this type of statement. +* Fix[2306] - Adds support for Trino UDF +* Trino SQL allows you to specify User Defined functions in the WITH clause ahead of a statement. This adds support for this type of statement. +* --------- +* Co-authored-by: David Hayes <dhayes@adobe.com> + +[6ce95](https://github.com/JSQLParser/JSqlParser/commit/6ce95d54378a2f6) David Hayes *2025-09-13 23:29:04* + +**support opengauss "on duplicate key update nothing" grammar (#2303)** + +* support opengauss "on duplicate key nothing" grammar +* use import single classes instead of wildcard imports in Insert class. +* use import single classes instead of wildcard imports in Insert class. +* use './gradlew :spotlessApply' adjust code format. +* --------- +* Co-authored-by: zhangkenan <zhangkenan@hexadb.com> + +[468ae](https://github.com/JSQLParser/JSqlParser/commit/468aefae04b0204) ConanZhang *2025-08-28 12:15:36* + +**Fix[2290] - Fix overeager lambda function parsing (#2293)** + +* `SELECT DATE_TRUNC('week',("schema"."tbl"."column" + INTERVAL '1 day')) FROM "schema"."tbl";` +* is parsing the `("schema"."tbl"."column" + INTERVAL '1 day')` as a LambdaFunction incorrectly, and crashes out. Increasing the lookahead depth by 1 ensures it fails to match on the `->` keyword (I believe), and falls into a simple expression instead. +* JMH +* ``` +* jmh { +* includes = ['.*JSQLParserBenchmark.*'] +* warmupIterations = 2 +* fork = 5 +* iterations = 5 +* timeOnIteration = '5s' +* } +* ``` +* After: +* ``` +* 33.970 ±(99.9%) 1.773 ms/op [Average] +* (min, avg, max) = (31.405, 33.970, 37.302), stdev = 2.367 +* CI (99.9%): [32.197, 35.743] (assumes normal distribution) +* ``` +* Before: +* ``` +* 34.882 ±(99.9%) 1.923 ms/op [Average] +* (min, avg, max) = (31.191, 34.882, 37.406), stdev = 2.567 +* CI (99.9%): [32.959, 36.805] (assumes normal distribution) +* ``` +* Co-authored-by: David Hayes <dhayes@adobe.com> + +[12489](https://github.com/JSQLParser/JSqlParser/commit/12489af67cb82dc) David Hayes *2025-08-11 22:49:52* + +**Fix[2288] - Support parenthesed expressions within Between (#2289)** + +* Supports the SQL of the form: +* ```sql +* SELECT * FROM tbl WHERE +* day BETWEEN +* CAST(CAST((NOW() + INTERVAL '-30 day') AS date) AS timestamptz) +* AND +* CAST(CAST((NOW() + INTERVAL '-1 day') AS date) AS timestamptz); +* ``` +* Co-authored-by: David Hayes <dhayes@adobe.com> + +[f10b5](https://github.com/JSQLParser/JSqlParser/commit/f10b52eda68da22) David Hayes *2025-08-05 14:57:46* + +**Fix[2283] - Fix broken ParserKeywordsUtilsTest (#2286)** + +* Javacc java generator was only being included as a pom artefact in test, but the jar is required for the ParserKeywordsUtilsTest to succeed. +* Co-authored-by: David Hayes <dhayes@adobe.com> + +[2ff73](https://github.com/JSQLParser/JSqlParser/commit/2ff7342dfe41435) David Hayes *2025-08-01 09:39:30* + +**Add pluginRepositories to pom.xml (#2284)** + +* Adds pluginRepositories to the pom.xml to allow the javacc plugin to not download from insecure repositories (https vs http). +* Also adds missing copyrights which were added by maven. +* Co-authored-by: David Hayes <dhayes@adobe.com> + +[f10e5](https://github.com/JSQLParser/JSqlParser/commit/f10e5bdd4fdd31c) David Hayes *2025-07-31 13:16:04* + +**added new maven central deployer** + + +[50e75](https://github.com/JSQLParser/JSqlParser/commit/50e75d6d64958b3) tw *2025-07-10 23:38:15* + +**added new maven central deployer** + + +[afc7b](https://github.com/JSQLParser/JSqlParser/commit/afc7bdb26eb53e7) tw *2025-07-10 23:04:43* + ## jsqlparser-5.3 (2025-05-17) ### Features -- eliminate another expensive syntactic lookahead ([6049f](https://github.com/JSQLParser/JSqlParser/commit/6049fd729ec4073) manticore-projects) -- avoid looping through the tokens every single time ([ac175](https://github.com/JSQLParser/JSqlParser/commit/ac175138405726b) manticore-projects) -- adopt proper JMH based benchmark ([fe860](https://github.com/JSQLParser/JSqlParser/commit/fe860ddd18f28f8) manticore-projects) -- Optimise performance ([7d42f](https://github.com/JSQLParser/JSqlParser/commit/7d42ff614bd2cca) manticore-projects) +- eliminate another expensive syntactic lookahead ([6049f](https://github.com/JSQLParser/JSqlParser/commit/6049fd729ec4073) manticore-projects) +- avoid looping through the tokens every single time ([ac175](https://github.com/JSQLParser/JSqlParser/commit/ac175138405726b) manticore-projects) +- adopt proper JMH based benchmark ([fe860](https://github.com/JSQLParser/JSqlParser/commit/fe860ddd18f28f8) manticore-projects) +- Optimise performance ([7d42f](https://github.com/JSQLParser/JSqlParser/commit/7d42ff614bd2cca) manticore-projects) ### Other changes @@ -41,62 +913,62 @@ Changelog of JSqlParser. ### Features -- enhance ALTER TABLE support with UNIQUE KEY and INVISIBLE index (#2234) ([78318](https://github.com/JSQLParser/JSqlParser/commit/78318bb64bcfc96) Minjae Lee) -- link all Expression AST Nodes ([98e03](https://github.com/JSQLParser/JSqlParser/commit/98e03d4c5e5f659) manticore-projects) -- add support for parsing MySQL ALTER TABLE statements with new options (#2223) ([9a36a](https://github.com/JSQLParser/JSqlParser/commit/9a36a48087eb311) Minjae Lee) -- support MySQL ALTER TABLE ... PARTITION BY syntax (#2210) ([d60c7](https://github.com/JSQLParser/JSqlParser/commit/d60c762f1d71c9f) Minjae Lee) -- add support SELECT all columns from function result (#2207) ([30cf5](https://github.com/JSQLParser/JSqlParser/commit/30cf5d7b930ae0a) Lian Hu) -- `WithItem` must accept statements too for supporting `Delete`, `Insert`, `Update` with `Returning` ([585b6](https://github.com/JSQLParser/JSqlParser/commit/585b69c9149d418) manticore-projects) -- allow simple expressions for `JsonExpression` ([eef8e](https://github.com/JSQLParser/JSqlParser/commit/eef8e6e08ac39c9) Andreas Reichel) -- System.getProperty("SPLIT_NAMES_ON_DELIMITER") for allowing `.` dots in Table Names ([1d37e](https://github.com/JSQLParser/JSqlParser/commit/1d37eb0b94effdb) Andreas Reichel) -- Nested `WithItem` allows `FromQuery` ([0f749](https://github.com/JSQLParser/JSqlParser/commit/0f7493fa3d63af8) Andreas Reichel) -- add Order Suffixes to `AggregatePipeOperator` and support some weird BigQuery Syntax ([f932b](https://github.com/JSQLParser/JSqlParser/commit/f932b8f9807530c) Andreas Reichel) -- More lenient `FromQuery` allowing for `Join` and `WithItem` ([bcac0](https://github.com/JSQLParser/JSqlParser/commit/bcac023a5e43297) Andreas Reichel) -- More lenient `FromQuery` allowing for `Join` and `WithItem` ([5c2a4](https://github.com/JSQLParser/JSqlParser/commit/5c2a4537506a632) Andreas Reichel) -- `SELECT` piper operator to support `ALL | DISTINCT` ([5b209](https://github.com/JSQLParser/JSqlParser/commit/5b209f5477464ec) Andreas Reichel) -- improve TableSample Operator ([19f0a](https://github.com/JSQLParser/JSqlParser/commit/19f0aa4762667d0) Andreas Reichel) -- rewrite Piped SQL, WIP ([47583](https://github.com/JSQLParser/JSqlParser/commit/47583dea7e23992) Andreas Reichel) -- rewrite Piped SQL, WIP ([519b0](https://github.com/JSQLParser/JSqlParser/commit/519b02040af3ad6) Andreas Reichel) -- rewrite Piped SQL, WIP ([1cb26](https://github.com/JSQLParser/JSqlParser/commit/1cb26a731efae14) Andreas Reichel) -- rewrite Piped SQL, WIP ([0e488](https://github.com/JSQLParser/JSqlParser/commit/0e488b05159745b) Andreas Reichel) -- `ParenthesedSelect` can have `Alias` ([b5b24](https://github.com/JSQLParser/JSqlParser/commit/b5b242a3f6648ce) Andreas Reichel) -- `ParenthesedSelect` can have `Alias` ([a510f](https://github.com/JSQLParser/JSqlParser/commit/a510f135d50964e) Andreas Reichel) -- rework the Visitors ([8c051](https://github.com/JSQLParser/JSqlParser/commit/8c05188d74f50c0) Andreas Reichel) -- rework the Visitors ([d8162](https://github.com/JSQLParser/JSqlParser/commit/d816220f155d5a3) Andreas Reichel) -- rework the Visitors ([13879](https://github.com/JSQLParser/JSqlParser/commit/1387986effd76f3) Andreas Reichel) -- parsing Piped SQL, complete all Operators ([48cc2](https://github.com/JSQLParser/JSqlParser/commit/48cc2f748eb5685) Andreas Reichel) -- parsing Piped SQL ([f687f](https://github.com/JSQLParser/JSqlParser/commit/f687f77d38e9328) Andreas Reichel) -- Support not equals operator with a hat (#2153) ([3fee9](https://github.com/JSQLParser/JSqlParser/commit/3fee9aa9a2d621d) Stefan Steinhauser) -- Support unknown keyword (#2141) ([9ab1e](https://github.com/JSQLParser/JSqlParser/commit/9ab1ebded5fa493) Emily Ong) -- serialize `WithItem` ([df665](https://github.com/JSQLParser/JSqlParser/commit/df66569a87ce6fb) manticore-projects) -- Adding coverage for Postgres "OVERRIDING SYSTEM VALUE" (#2142) ([0ab13](https://github.com/JSQLParser/JSqlParser/commit/0ab131922d972aa) nicky6s) -- Piped SQL and FROM queries (WIP) ([01dc5](https://github.com/JSQLParser/JSqlParser/commit/01dc5f3472ce5ec) manticore-projects) -- Piped SQL and FROM queries (WIP) ([abf13](https://github.com/JSQLParser/JSqlParser/commit/abf137e04ca765d) manticore-projects) -- Piped SQL and FROM queries (WIP) ([f25e2](https://github.com/JSQLParser/JSqlParser/commit/f25e28bff4c96cb) manticore-projects) +- enhance ALTER TABLE support with UNIQUE KEY and INVISIBLE index (#2234) ([78318](https://github.com/JSQLParser/JSqlParser/commit/78318bb64bcfc96) Minjae Lee) +- link all Expression AST Nodes ([98e03](https://github.com/JSQLParser/JSqlParser/commit/98e03d4c5e5f659) manticore-projects) +- add support for parsing MySQL ALTER TABLE statements with new options (#2223) ([9a36a](https://github.com/JSQLParser/JSqlParser/commit/9a36a48087eb311) Minjae Lee) +- support MySQL ALTER TABLE ... PARTITION BY syntax (#2210) ([d60c7](https://github.com/JSQLParser/JSqlParser/commit/d60c762f1d71c9f) Minjae Lee) +- add support SELECT all columns from function result (#2207) ([30cf5](https://github.com/JSQLParser/JSqlParser/commit/30cf5d7b930ae0a) Lian Hu) +- `WithItem` must accept statements too for supporting `Delete`, `Insert`, `Update` with `Returning` ([585b6](https://github.com/JSQLParser/JSqlParser/commit/585b69c9149d418) manticore-projects) +- allow simple expressions for `JsonExpression` ([eef8e](https://github.com/JSQLParser/JSqlParser/commit/eef8e6e08ac39c9) Andreas Reichel) +- System.getProperty("SPLIT_NAMES_ON_DELIMITER") for allowing `.` dots in Table Names ([1d37e](https://github.com/JSQLParser/JSqlParser/commit/1d37eb0b94effdb) Andreas Reichel) +- Nested `WithItem` allows `FromQuery` ([0f749](https://github.com/JSQLParser/JSqlParser/commit/0f7493fa3d63af8) Andreas Reichel) +- add Order Suffixes to `AggregatePipeOperator` and support some weird BigQuery Syntax ([f932b](https://github.com/JSQLParser/JSqlParser/commit/f932b8f9807530c) Andreas Reichel) +- More lenient `FromQuery` allowing for `Join` and `WithItem` ([bcac0](https://github.com/JSQLParser/JSqlParser/commit/bcac023a5e43297) Andreas Reichel) +- More lenient `FromQuery` allowing for `Join` and `WithItem` ([5c2a4](https://github.com/JSQLParser/JSqlParser/commit/5c2a4537506a632) Andreas Reichel) +- `SELECT` piper operator to support `ALL | DISTINCT` ([5b209](https://github.com/JSQLParser/JSqlParser/commit/5b209f5477464ec) Andreas Reichel) +- improve TableSample Operator ([19f0a](https://github.com/JSQLParser/JSqlParser/commit/19f0aa4762667d0) Andreas Reichel) +- rewrite Piped SQL, WIP ([47583](https://github.com/JSQLParser/JSqlParser/commit/47583dea7e23992) Andreas Reichel) +- rewrite Piped SQL, WIP ([519b0](https://github.com/JSQLParser/JSqlParser/commit/519b02040af3ad6) Andreas Reichel) +- rewrite Piped SQL, WIP ([1cb26](https://github.com/JSQLParser/JSqlParser/commit/1cb26a731efae14) Andreas Reichel) +- rewrite Piped SQL, WIP ([0e488](https://github.com/JSQLParser/JSqlParser/commit/0e488b05159745b) Andreas Reichel) +- `ParenthesedSelect` can have `Alias` ([b5b24](https://github.com/JSQLParser/JSqlParser/commit/b5b242a3f6648ce) Andreas Reichel) +- `ParenthesedSelect` can have `Alias` ([a510f](https://github.com/JSQLParser/JSqlParser/commit/a510f135d50964e) Andreas Reichel) +- rework the Visitors ([8c051](https://github.com/JSQLParser/JSqlParser/commit/8c05188d74f50c0) Andreas Reichel) +- rework the Visitors ([d8162](https://github.com/JSQLParser/JSqlParser/commit/d816220f155d5a3) Andreas Reichel) +- rework the Visitors ([13879](https://github.com/JSQLParser/JSqlParser/commit/1387986effd76f3) Andreas Reichel) +- parsing Piped SQL, complete all Operators ([48cc2](https://github.com/JSQLParser/JSqlParser/commit/48cc2f748eb5685) Andreas Reichel) +- parsing Piped SQL ([f687f](https://github.com/JSQLParser/JSqlParser/commit/f687f77d38e9328) Andreas Reichel) +- Support not equals operator with a hat (#2153) ([3fee9](https://github.com/JSQLParser/JSqlParser/commit/3fee9aa9a2d621d) Stefan Steinhauser) +- Support unknown keyword (#2141) ([9ab1e](https://github.com/JSQLParser/JSqlParser/commit/9ab1ebded5fa493) Emily Ong) +- serialize `WithItem` ([df665](https://github.com/JSQLParser/JSqlParser/commit/df66569a87ce6fb) manticore-projects) +- Adding coverage for Postgres "OVERRIDING SYSTEM VALUE" (#2142) ([0ab13](https://github.com/JSQLParser/JSqlParser/commit/0ab131922d972aa) nicky6s) +- Piped SQL and FROM queries (WIP) ([01dc5](https://github.com/JSQLParser/JSqlParser/commit/01dc5f3472ce5ec) manticore-projects) +- Piped SQL and FROM queries (WIP) ([abf13](https://github.com/JSQLParser/JSqlParser/commit/abf137e04ca765d) manticore-projects) +- Piped SQL and FROM queries (WIP) ([f25e2](https://github.com/JSQLParser/JSqlParser/commit/f25e28bff4c96cb) manticore-projects) ### Bug Fixes -- `GRANT` to `PUBLIC` (allow keyword) ([4c4ff](https://github.com/JSQLParser/JSqlParser/commit/4c4ff286434f35e) manticore-projects) -- Oracle Hierarchical Expression shall allow XOR (not AND only) ([c9c84](https://github.com/JSQLParser/JSqlParser/commit/c9c844aa22d1bbc) manticore-projects) -- grammar ambiguities from #2217 ([b409c](https://github.com/JSQLParser/JSqlParser/commit/b409ceb5aa2c9d6) Andreas Reichel) -- SelectItem generics ([b73ce](https://github.com/JSQLParser/JSqlParser/commit/b73cee65cd74710) Andreas Reichel) -- add some public getters ([09f3d](https://github.com/JSQLParser/JSqlParser/commit/09f3dbcd1d8ad44) Andreas Reichel) -- avoid NPE on empty Window Element properties ([16685](https://github.com/JSQLParser/JSqlParser/commit/16685289d7d5879) Andreas Reichel) -- `IntervalExpression` supports complex expressions ([e30f3](https://github.com/JSQLParser/JSqlParser/commit/e30f30d27b58325) Andreas Reichel) -- avoid one JavaCC ambiguity warning ([0c092](https://github.com/JSQLParser/JSqlParser/commit/0c092afff549d52) Andreas Reichel) -- avoid NPE ([9714f](https://github.com/JSQLParser/JSqlParser/commit/9714f940d249af9) Andreas Reichel) -- Remove unneeded `LOOKAHEAD` ([fb978](https://github.com/JSQLParser/JSqlParser/commit/fb978ce11a81655) Andreas Reichel) -- `Like`, better use `SimpleExpression` only else there are precedence issues ([1d594](https://github.com/JSQLParser/JSqlParser/commit/1d59430eeeaf67e) Andreas Reichel) -- `Like` Expression accepts Complex Expressions including `BY PRIOR` ([9b92c](https://github.com/JSQLParser/JSqlParser/commit/9b92cdfcec7206c) Andreas Reichel) -- use `SelectItem` for `UnPivot` ([b1b86](https://github.com/JSQLParser/JSqlParser/commit/b1b86f549529ab6) Andreas Reichel) -- move `Pivot` and `UnPivot` before `OrderBy` ([9c37f](https://github.com/JSQLParser/JSqlParser/commit/9c37fe8eb8f3d42) Andreas Reichel) -- move `Pivot` and `UnPivot` before `OrderBy` ([2ca16](https://github.com/JSQLParser/JSqlParser/commit/2ca167a90f59c6c) Andreas Reichel) -- use `Function` and `SelectItem` ([931d3](https://github.com/JSQLParser/JSqlParser/commit/931d376ed9adc2a) Andreas Reichel) -- use `Function` and `SelectItem` ([db008](https://github.com/JSQLParser/JSqlParser/commit/db0081d18ba4b19) Andreas Reichel) -- `AllColumns` replace rewrite ([b366f](https://github.com/JSQLParser/JSqlParser/commit/b366f2b0a4e6ab6) Andreas Reichel) -- use proper tokens `..` and `...` in multi-part names for correct white space handling ([38d0e](https://github.com/JSQLParser/JSqlParser/commit/38d0e36e3335dab) Andreas Reichel) -- `AllColumns` replace takes expressions ([a2f40](https://github.com/JSQLParser/JSqlParser/commit/a2f40dd5f79fdef) Andreas Reichel) -- allowing skyline keywords as columns, tables, etc. (#2137) ([f9b1c](https://github.com/JSQLParser/JSqlParser/commit/f9b1cbd8a8e1c8d) nicky6s) +- `GRANT` to `PUBLIC` (allow keyword) ([4c4ff](https://github.com/JSQLParser/JSqlParser/commit/4c4ff286434f35e) manticore-projects) +- Oracle Hierarchical Expression shall allow XOR (not AND only) ([c9c84](https://github.com/JSQLParser/JSqlParser/commit/c9c844aa22d1bbc) manticore-projects) +- grammar ambiguities from #2217 ([b409c](https://github.com/JSQLParser/JSqlParser/commit/b409ceb5aa2c9d6) Andreas Reichel) +- SelectItem generics ([b73ce](https://github.com/JSQLParser/JSqlParser/commit/b73cee65cd74710) Andreas Reichel) +- add some public getters ([09f3d](https://github.com/JSQLParser/JSqlParser/commit/09f3dbcd1d8ad44) Andreas Reichel) +- avoid NPE on empty Window Element properties ([16685](https://github.com/JSQLParser/JSqlParser/commit/16685289d7d5879) Andreas Reichel) +- `IntervalExpression` supports complex expressions ([e30f3](https://github.com/JSQLParser/JSqlParser/commit/e30f30d27b58325) Andreas Reichel) +- avoid one JavaCC ambiguity warning ([0c092](https://github.com/JSQLParser/JSqlParser/commit/0c092afff549d52) Andreas Reichel) +- avoid NPE ([9714f](https://github.com/JSQLParser/JSqlParser/commit/9714f940d249af9) Andreas Reichel) +- Remove unneeded `LOOKAHEAD` ([fb978](https://github.com/JSQLParser/JSqlParser/commit/fb978ce11a81655) Andreas Reichel) +- `Like`, better use `SimpleExpression` only else there are precedence issues ([1d594](https://github.com/JSQLParser/JSqlParser/commit/1d59430eeeaf67e) Andreas Reichel) +- `Like` Expression accepts Complex Expressions including `BY PRIOR` ([9b92c](https://github.com/JSQLParser/JSqlParser/commit/9b92cdfcec7206c) Andreas Reichel) +- use `SelectItem` for `UnPivot` ([b1b86](https://github.com/JSQLParser/JSqlParser/commit/b1b86f549529ab6) Andreas Reichel) +- move `Pivot` and `UnPivot` before `OrderBy` ([9c37f](https://github.com/JSQLParser/JSqlParser/commit/9c37fe8eb8f3d42) Andreas Reichel) +- move `Pivot` and `UnPivot` before `OrderBy` ([2ca16](https://github.com/JSQLParser/JSqlParser/commit/2ca167a90f59c6c) Andreas Reichel) +- use `Function` and `SelectItem` ([931d3](https://github.com/JSQLParser/JSqlParser/commit/931d376ed9adc2a) Andreas Reichel) +- use `Function` and `SelectItem` ([db008](https://github.com/JSQLParser/JSqlParser/commit/db0081d18ba4b19) Andreas Reichel) +- `AllColumns` replace rewrite ([b366f](https://github.com/JSQLParser/JSqlParser/commit/b366f2b0a4e6ab6) Andreas Reichel) +- use proper tokens `..` and `...` in multi-part names for correct white space handling ([38d0e](https://github.com/JSQLParser/JSqlParser/commit/38d0e36e3335dab) Andreas Reichel) +- `AllColumns` replace takes expressions ([a2f40](https://github.com/JSQLParser/JSqlParser/commit/a2f40dd5f79fdef) Andreas Reichel) +- allowing skyline keywords as columns, tables, etc. (#2137) ([f9b1c](https://github.com/JSQLParser/JSqlParser/commit/f9b1cbd8a8e1c8d) nicky6s) ### Other changes @@ -117,74 +989,74 @@ Changelog of JSqlParser. **Add support for TableFunction [WITH OFFSET|ORDINALITY] (#2219)** -* Add support for TableFunction [WITH OFFSET|ORDINALITY] -* This PR fixes #2218, by adding a new `withClause` to a TableFunction, which allows for support of SQL-99's `UNNEST(...) WITH ORDINALITY` or GoogleSQL's `UNNEST(...) WITH OFFSET`. -* Currently, this is a feeler PR, as I'm getting a test failure, as the output of the parsing differs in my tests: -* ``` -* org.opentest4j.AssertionFailedError: Output from Deparser does not match. ==> -* Expected :select*from unnest(array[1,2,3])with offset as t(a,b) -* Actual :select*from unnest(array[1,2,3])as t(a,b) -* <Click to see difference> -* at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151) -* at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132) -* at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197) -* at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182) -* at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1156) -* at net.sf.jsqlparser@5.2-SNAPSHOT/net.sf.jsqlparser.test.TestUtils.assertStatementCanBeDeparsedAs(TestUtils.java:126) -* at net.sf.jsqlparser@5.2-SNAPSHOT/net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed(TestUtils.java:99) -* at net.sf.jsqlparser@5.2-SNAPSHOT/net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed(TestUtils.java:85) -* at net.sf.jsqlparser@5.2-SNAPSHOT/net.sf.jsqlparser.statement.select.TableFunctionTest.testTableFunctionWithSupportedWithClauses(TableFunctionTest.java:60) -* at java.base/java.lang.reflect.Method.invoke(Method.java:580) -* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) -* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) -* at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179) -* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) -* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) -* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) -* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) -* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) -* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) -* at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:1024) -* at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) -* at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) -* at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) -* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) -* at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) -* at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) -* at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276) -* at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708) -* at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) -* at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) -* at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) -* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) -* at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) -* at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) -* at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276) -* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) -* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) -* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) -* at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708) -* at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) -* at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) -* at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) -* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) -* at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) -* at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) -* at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276) -* at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708) -* at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) -* at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) -* at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) -* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) -* at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) -* at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) -* ``` -* rename clause -> withClause -* Resolving test failure, and modifying the SelectDeParser to add the withClause where present -* Addressing PR Review comments. Using existing WITH and OFFSET keywords, adding ORDINALITY keyword. -* Removing ORDINALITY from the reserved keywords list in ParserKeywordsUtils, and updating pushing the result of `gradle updateKeywords` -* --------- -* Co-authored-by: David Hayes <dhayes@adobe.com> +* Add support for TableFunction [WITH OFFSET|ORDINALITY] +* This PR fixes #2218, by adding a new `withClause` to a TableFunction, which allows for support of SQL-99's `UNNEST(...) WITH ORDINALITY` or GoogleSQL's `UNNEST(...) WITH OFFSET`. +* Currently, this is a feeler PR, as I'm getting a test failure, as the output of the parsing differs in my tests: +* ``` +* org.opentest4j.AssertionFailedError: Output from Deparser does not match. ==> +* Expected :select*from unnest(array[1,2,3])with offset as t(a,b) +* Actual :select*from unnest(array[1,2,3])as t(a,b) +* <Click to see difference> +* at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151) +* at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132) +* at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197) +* at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182) +* at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1156) +* at net.sf.jsqlparser@5.2-SNAPSHOT/net.sf.jsqlparser.test.TestUtils.assertStatementCanBeDeparsedAs(TestUtils.java:126) +* at net.sf.jsqlparser@5.2-SNAPSHOT/net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed(TestUtils.java:99) +* at net.sf.jsqlparser@5.2-SNAPSHOT/net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed(TestUtils.java:85) +* at net.sf.jsqlparser@5.2-SNAPSHOT/net.sf.jsqlparser.statement.select.TableFunctionTest.testTableFunctionWithSupportedWithClauses(TableFunctionTest.java:60) +* at java.base/java.lang.reflect.Method.invoke(Method.java:580) +* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) +* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) +* at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179) +* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) +* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) +* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) +* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) +* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) +* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) +* at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:1024) +* at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) +* at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) +* at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) +* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) +* at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) +* at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) +* at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276) +* at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708) +* at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) +* at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) +* at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) +* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) +* at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) +* at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) +* at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276) +* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) +* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) +* at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) +* at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708) +* at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) +* at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) +* at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) +* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) +* at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) +* at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) +* at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276) +* at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708) +* at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) +* at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) +* at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) +* at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) +* at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) +* at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) +* ``` +* rename clause -> withClause +* Resolving test failure, and modifying the SelectDeParser to add the withClause where present +* Addressing PR Review comments. Using existing WITH and OFFSET keywords, adding ORDINALITY keyword. +* Removing ORDINALITY from the reserved keywords list in ParserKeywordsUtils, and updating pushing the result of `gradle updateKeywords` +* --------- +* Co-authored-by: David Hayes <dhayes@adobe.com> [e5fa0](https://github.com/JSQLParser/JSqlParser/commit/e5fa004cdffdcc8) David Hayes *2025-04-08 09:22:59* @@ -195,11 +1067,11 @@ Changelog of JSqlParser. **Add MySQL Index Parsing Support for ALTER TABLE Statements (#2215)** -* add spatial, fulltext index -* add complex spatial index case -* fix Reflection test -* add test case about add index -* fix for passing test +* add spatial, fulltext index +* add complex spatial index case +* fix Reflection test +* add test case about add index +* fix for passing test [090bb](https://github.com/JSQLParser/JSqlParser/commit/090bb60da58c3f4) Minjae Lee *2025-04-04 12:32:44* @@ -235,16 +1107,16 @@ Changelog of JSqlParser. **Add support for parsing MySQL DISCARD and IMPORT TABLESPACE DDL statements (#2198)** -* Add support for parsing MySQL DISCARD and IMPORT TABLESPACE DDL statements -* fix code formatting -* --------- -* Co-authored-by: mj-db <mj.db@kakaocorp.com> +* Add support for parsing MySQL DISCARD and IMPORT TABLESPACE DDL statements +* fix code formatting +* --------- +* Co-authored-by: mj-db <mj.db@kakaocorp.com> [8e651](https://github.com/JSQLParser/JSqlParser/commit/8e651ff232171eb) Minjae Lee *2025-03-20 23:44:30* **IsNullExpression and IsBooleanExpression children are not visited in TablesNamesFinder (#2180)** -* IsNullExpression and IsBooleanExpression children are not visited in TablesNamesFinder +* IsNullExpression and IsBooleanExpression children are not visited in TablesNamesFinder [ad575](https://github.com/JSQLParser/JSqlParser/commit/ad5758048f06462) SuperPat *2025-03-04 14:07:39* @@ -255,16 +1127,16 @@ Changelog of JSqlParser. **added support for minus all; except all; intersect all; (#2081)** -* Co-authored-by: sunanda-vs <22m2107@iitb.ac.in> +* Co-authored-by: sunanda-vs <22m2107@iitb.ac.in> [4492e](https://github.com/JSQLParser/JSqlParser/commit/4492e74fd36f8a4) Sunanda Somwase *2025-02-07 23:42:00* **#2119 add support INSERT OVERWRITE PARTITION (#2135)** -* #2119 add support INSERT OVERWRITE PARTITION -* #2119 fix test case -* #2119 add reserved keyword -* run spotlessApply +* #2119 add support INSERT OVERWRITE PARTITION +* #2119 fix test case +* #2119 add reserved keyword +* run spotlessApply [f1dee](https://github.com/JSQLParser/JSqlParser/commit/f1dee4c43a9a300) tt20061904 *2025-01-06 06:07:23* @@ -273,48 +1145,48 @@ Changelog of JSqlParser. ### Features -- allow file names as `Table` supporting CSV or Parquet files ([b2877](https://github.com/JSQLParser/JSqlParser/commit/b287779823e338b) manticore-projects) -- add support MATCH_ANY MATCH_ALL MATCH_PHRASE MATCH_PHRASE_PREFIX MATCH_REGEXP (#2132) ([f6c3c](https://github.com/JSQLParser/JSqlParser/commit/f6c3c7dc2a1b4ff) Liming Deng) -- syntax sugar for table functions ([e8ff9](https://github.com/JSQLParser/JSqlParser/commit/e8ff9fb98d299dc) manticore-projects) -- functions with extra keywords, like BigQuery Timeseries functions ([b0b0f](https://github.com/JSQLParser/JSqlParser/commit/b0b0f4d6178918b) manticore-projects) -- add JSON to the list of data types ([92bbd](https://github.com/JSQLParser/JSqlParser/commit/92bbd90e8755d8d) Andreas Reichel) -- proper `:` JSon operator ([f8080](https://github.com/JSQLParser/JSqlParser/commit/f80800ca5294e89) Andreas Reichel) -- Snowflake `GET` operator `:` ([6eb58](https://github.com/JSQLParser/JSqlParser/commit/6eb588752f0f780) Andreas Reichel) -- `LATERAL VIEW` supports multiple alias columns ([4814c](https://github.com/JSQLParser/JSqlParser/commit/4814ccdd35b4277) Andreas Reichel) -- `ON OVERFLOW` support for the `ListAgg` function (Oracle, DB2) ([5cf28](https://github.com/JSQLParser/JSqlParser/commit/5cf281f1de4f9d3) Andreas Reichel) -- `JsonExpression` supports Expressions rather than Char/Long only ([3d488](https://github.com/JSQLParser/JSqlParser/commit/3d4883523e255e9) Andreas Reichel) -- `CosineSimilarity` Expression ([90adf](https://github.com/JSQLParser/JSqlParser/commit/90adf829153e00f) Andreas Reichel) -- add syntax sugar for accessing name parts easier ([25b08](https://github.com/JSQLParser/JSqlParser/commit/25b08b15e3cc670) manticore-projects) -- add Oracle/H2 `EXPLAIN PLAN [FOR] select` ([d837b](https://github.com/JSQLParser/JSqlParser/commit/d837b677add70ef) manticore-projects) -- add DuckDB `SUMMARIZE table | select` ([ad132](https://github.com/JSQLParser/JSqlParser/commit/ad132ee298d7bfe) manticore-projects) -- methods for returning unquoted names and identifiers ([7e7ac](https://github.com/JSQLParser/JSqlParser/commit/7e7acba1b7d8e54) manticore-projects) -- methods for returning unquoted names and identifiers ([728b2](https://github.com/JSQLParser/JSqlParser/commit/728b286f0948aba) manticore-projects) -- `CREATE SCHEMA IF NOT EXISTS ...` ([d7bf2](https://github.com/JSQLParser/JSqlParser/commit/d7bf249b94c97ff) manticore-projects) -- improve the Expression Visitor Adapter ([e8bc4](https://github.com/JSQLParser/JSqlParser/commit/e8bc4463f907b75) Andreas Reichel) -- improve Visitors ([8a662](https://github.com/JSQLParser/JSqlParser/commit/8a662af8109b650) Andreas Reichel) -- syntax sugar ([13572](https://github.com/JSQLParser/JSqlParser/commit/13572a83b703324) Andreas Reichel) -- improve String representation of `Table` and `Column` ([f3268](https://github.com/JSQLParser/JSqlParser/commit/f3268e73bdd0040) Andreas Reichel) -- improve usability of the `ExpressionVisitorAdapter` ([6a36b](https://github.com/JSQLParser/JSqlParser/commit/6a36b3fc6af5ae1) Andreas Reichel) +- allow file names as `Table` supporting CSV or Parquet files ([b2877](https://github.com/JSQLParser/JSqlParser/commit/b287779823e338b) manticore-projects) +- add support MATCH_ANY MATCH_ALL MATCH_PHRASE MATCH_PHRASE_PREFIX MATCH_REGEXP (#2132) ([f6c3c](https://github.com/JSQLParser/JSqlParser/commit/f6c3c7dc2a1b4ff) Liming Deng) +- syntax sugar for table functions ([e8ff9](https://github.com/JSQLParser/JSqlParser/commit/e8ff9fb98d299dc) manticore-projects) +- functions with extra keywords, like BigQuery Timeseries functions ([b0b0f](https://github.com/JSQLParser/JSqlParser/commit/b0b0f4d6178918b) manticore-projects) +- add JSON to the list of data types ([92bbd](https://github.com/JSQLParser/JSqlParser/commit/92bbd90e8755d8d) Andreas Reichel) +- proper `:` JSon operator ([f8080](https://github.com/JSQLParser/JSqlParser/commit/f80800ca5294e89) Andreas Reichel) +- Snowflake `GET` operator `:` ([6eb58](https://github.com/JSQLParser/JSqlParser/commit/6eb588752f0f780) Andreas Reichel) +- `LATERAL VIEW` supports multiple alias columns ([4814c](https://github.com/JSQLParser/JSqlParser/commit/4814ccdd35b4277) Andreas Reichel) +- `ON OVERFLOW` support for the `ListAgg` function (Oracle, DB2) ([5cf28](https://github.com/JSQLParser/JSqlParser/commit/5cf281f1de4f9d3) Andreas Reichel) +- `JsonExpression` supports Expressions rather than Char/Long only ([3d488](https://github.com/JSQLParser/JSqlParser/commit/3d4883523e255e9) Andreas Reichel) +- `CosineSimilarity` Expression ([90adf](https://github.com/JSQLParser/JSqlParser/commit/90adf829153e00f) Andreas Reichel) +- add syntax sugar for accessing name parts easier ([25b08](https://github.com/JSQLParser/JSqlParser/commit/25b08b15e3cc670) manticore-projects) +- add Oracle/H2 `EXPLAIN PLAN [FOR] select` ([d837b](https://github.com/JSQLParser/JSqlParser/commit/d837b677add70ef) manticore-projects) +- add DuckDB `SUMMARIZE table | select` ([ad132](https://github.com/JSQLParser/JSqlParser/commit/ad132ee298d7bfe) manticore-projects) +- methods for returning unquoted names and identifiers ([7e7ac](https://github.com/JSQLParser/JSqlParser/commit/7e7acba1b7d8e54) manticore-projects) +- methods for returning unquoted names and identifiers ([728b2](https://github.com/JSQLParser/JSqlParser/commit/728b286f0948aba) manticore-projects) +- `CREATE SCHEMA IF NOT EXISTS ...` ([d7bf2](https://github.com/JSQLParser/JSqlParser/commit/d7bf249b94c97ff) manticore-projects) +- improve the Expression Visitor Adapter ([e8bc4](https://github.com/JSQLParser/JSqlParser/commit/e8bc4463f907b75) Andreas Reichel) +- improve Visitors ([8a662](https://github.com/JSQLParser/JSqlParser/commit/8a662af8109b650) Andreas Reichel) +- syntax sugar ([13572](https://github.com/JSQLParser/JSqlParser/commit/13572a83b703324) Andreas Reichel) +- improve String representation of `Table` and `Column` ([f3268](https://github.com/JSQLParser/JSqlParser/commit/f3268e73bdd0040) Andreas Reichel) +- improve usability of the `ExpressionVisitorAdapter` ([6a36b](https://github.com/JSQLParser/JSqlParser/commit/6a36b3fc6af5ae1) Andreas Reichel) ### Bug Fixes -- rewrite the production to make it comptaible with the Maven build ([785af](https://github.com/JSQLParser/JSqlParser/commit/785af163575f189) manticore-projects) -- [FEATURE] TablesNamesFinder does not support CREATE VIEW #2123 (#2124) ([8f843](https://github.com/JSQLParser/JSqlParser/commit/8f8439e485a412e) Cedric Dandoy) -- De-parse `TableFunction` ([e122b](https://github.com/JSQLParser/JSqlParser/commit/e122bcf7d7a01cf) manticore-projects) -- Alias of the ParenthesedSelect when FromItem is a TableFunction ([46919](https://github.com/JSQLParser/JSqlParser/commit/469190d03709254) manticore-projects) -- JSon functions with Column parameters ([82251](https://github.com/JSQLParser/JSqlParser/commit/822517899546f19) Andreas Reichel) -- Issue #2109 true and false value parsed as column instead of BooleanValue (#2110) ([7acf9](https://github.com/JSQLParser/JSqlParser/commit/7acf9d548edc854) lucarota) -- `CREATE TABLE` UNIQUE vs. UNIQUE KEY ([3030c](https://github.com/JSQLParser/JSqlParser/commit/3030c35d28ad3cf) Andreas Reichel) -- clean up minor issues around #2083 ([be8ff](https://github.com/JSQLParser/JSqlParser/commit/be8ff930ebc4fa0) Andreas Reichel) -- avoid NPE ([db4b0](https://github.com/JSQLParser/JSqlParser/commit/db4b04dd0b8ce28) manticore-projects) -- fix the Grammar so it builds with Maven ([d3d3a](https://github.com/JSQLParser/JSqlParser/commit/d3d3af9b5974fb5) manticore-projects) -- `TableNameFinder` for `Alter Table` ([33ba5](https://github.com/JSQLParser/JSqlParser/commit/33ba5eb521c4dae) manticore-projects) -- remove obsolete `SimpleFunction` ([cd068](https://github.com/JSQLParser/JSqlParser/commit/cd0689e0120fc8e) manticore-projects) -- license headers ([e842e](https://github.com/JSQLParser/JSqlParser/commit/e842e18511c65f5) Andreas Reichel) -- add needed LOOKAHEAD(2) ([421e2](https://github.com/JSQLParser/JSqlParser/commit/421e2894b226018) Andreas Reichel) -- TablesNamesFinder `UpdateSets` ([11ceb](https://github.com/JSQLParser/JSqlParser/commit/11cebcfd1220944) Andreas Reichel) -- Multi-Variable `LambdaExpression` ([e0ad7](https://github.com/JSQLParser/JSqlParser/commit/e0ad7c84b688c39) Andreas Reichel) -- `FromItem` alias must not shade an actual table (found before) ([d8207](https://github.com/JSQLParser/JSqlParser/commit/d8207aaf616f10e) Andreas Reichel) +- rewrite the production to make it comptaible with the Maven build ([785af](https://github.com/JSQLParser/JSqlParser/commit/785af163575f189) manticore-projects) +- [FEATURE] TablesNamesFinder does not support CREATE VIEW #2123 (#2124) ([8f843](https://github.com/JSQLParser/JSqlParser/commit/8f8439e485a412e) Cedric Dandoy) +- De-parse `TableFunction` ([e122b](https://github.com/JSQLParser/JSqlParser/commit/e122bcf7d7a01cf) manticore-projects) +- Alias of the ParenthesedSelect when FromItem is a TableFunction ([46919](https://github.com/JSQLParser/JSqlParser/commit/469190d03709254) manticore-projects) +- JSon functions with Column parameters ([82251](https://github.com/JSQLParser/JSqlParser/commit/822517899546f19) Andreas Reichel) +- Issue #2109 true and false value parsed as column instead of BooleanValue (#2110) ([7acf9](https://github.com/JSQLParser/JSqlParser/commit/7acf9d548edc854) lucarota) +- `CREATE TABLE` UNIQUE vs. UNIQUE KEY ([3030c](https://github.com/JSQLParser/JSqlParser/commit/3030c35d28ad3cf) Andreas Reichel) +- clean up minor issues around #2083 ([be8ff](https://github.com/JSQLParser/JSqlParser/commit/be8ff930ebc4fa0) Andreas Reichel) +- avoid NPE ([db4b0](https://github.com/JSQLParser/JSqlParser/commit/db4b04dd0b8ce28) manticore-projects) +- fix the Grammar so it builds with Maven ([d3d3a](https://github.com/JSQLParser/JSqlParser/commit/d3d3af9b5974fb5) manticore-projects) +- `TableNameFinder` for `Alter Table` ([33ba5](https://github.com/JSQLParser/JSqlParser/commit/33ba5eb521c4dae) manticore-projects) +- remove obsolete `SimpleFunction` ([cd068](https://github.com/JSQLParser/JSqlParser/commit/cd0689e0120fc8e) manticore-projects) +- license headers ([e842e](https://github.com/JSQLParser/JSqlParser/commit/e842e18511c65f5) Andreas Reichel) +- add needed LOOKAHEAD(2) ([421e2](https://github.com/JSQLParser/JSqlParser/commit/421e2894b226018) Andreas Reichel) +- TablesNamesFinder `UpdateSets` ([11ceb](https://github.com/JSQLParser/JSqlParser/commit/11cebcfd1220944) Andreas Reichel) +- Multi-Variable `LambdaExpression` ([e0ad7](https://github.com/JSQLParser/JSqlParser/commit/e0ad7c84b688c39) Andreas Reichel) +- `FromItem` alias must not shade an actual table (found before) ([d8207](https://github.com/JSQLParser/JSqlParser/commit/d8207aaf616f10e) Andreas Reichel) ### Other changes @@ -345,7 +1217,7 @@ Changelog of JSqlParser. **feat mysql alter force,engine,algorithm,lock (#2121)** -* Co-authored-by: mj-db <mj.db@kakaocorp.com> +* Co-authored-by: mj-db <mj.db@kakaocorp.com> [18c1a](https://github.com/JSQLParser/JSqlParser/commit/18c1a2c63d9719b) Minjae Lee *2024-12-09 09:09:37* @@ -361,111 +1233,111 @@ Changelog of JSqlParser. **Fix issue 2089: Enhance MySQL CONVERT Statement Parsing (#2117)** -* fix convert toString -* fix mysql convert -* --------- -* Co-authored-by: mj-db <mj.db@kakaocorp.com> +* fix convert toString +* fix mysql convert +* --------- +* Co-authored-by: mj-db <mj.db@kakaocorp.com> [8b414](https://github.com/JSQLParser/JSqlParser/commit/8b4149b3b7e9a35) Minjae Lee *2024-11-26 05:55:37* **Fix issue 2106: Add parsing functionality for MySQL ALTER Table option statements (#2115)** -* add alter table option -* fix CreateParameter by adding auto_increment for passing test -* fix by updatekeywords -* test add assertSqlCanBeParsedAndDeparsed -* --------- -* Co-authored-by: mj-db <mj.db@kakaocorp.com> +* add alter table option +* fix CreateParameter by adding auto_increment for passing test +* fix by updatekeywords +* test add assertSqlCanBeParsedAndDeparsed +* --------- +* Co-authored-by: mj-db <mj.db@kakaocorp.com> [fee2f](https://github.com/JSQLParser/JSqlParser/commit/fee2f90c0283b3e) Minjae Lee *2024-11-24 00:46:00* **Fix issue 2106: Add parsing functionality for MySQL ADD PARTITION and DROP PARTITION clauses in ALTER TABLE statements(2) (#2108)** -* feat MySQL Alter add partition -* fix PartitionDefinition to Serializable -* add Engine variable to MySQL partition definition -* fix: Update parser to correctly handle ENGINE token and pass existing CREATE TABLE tests -* feat mysql alter drop partition -* refactor truncate partition -* fix codacy -* fix: add LOOKAHEADs -* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> -* doc: mention running `gradle check` -* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> -* style: add license header -* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> -* --------- -* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> -* Co-authored-by: mj-db <mj.db@kakaocorp.com> -* Co-authored-by: Andreas Reichel <andreas@manticore-projects.com> +* feat MySQL Alter add partition +* fix PartitionDefinition to Serializable +* add Engine variable to MySQL partition definition +* fix: Update parser to correctly handle ENGINE token and pass existing CREATE TABLE tests +* feat mysql alter drop partition +* refactor truncate partition +* fix codacy +* fix: add LOOKAHEADs +* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> +* doc: mention running `gradle check` +* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> +* style: add license header +* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> +* --------- +* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> +* Co-authored-by: mj-db <mj.db@kakaocorp.com> +* Co-authored-by: Andreas Reichel <andreas@manticore-projects.com> [12952](https://github.com/JSQLParser/JSqlParser/commit/12952d64f87bdef) Minjae Lee *2024-11-14 01:05:19* **Fix issue 2106: Add parsing functionality for MySQL `ADD PARTITION` and `DROP PARTITION` clauses in `ALTER TABLE` statements (#2107)** -* feat MySQL Alter add partition -* fix PartitionDefinition to Serializable -* --------- -* Co-authored-by: mj-db <mj.db@kakaocorp.com> +* feat MySQL Alter add partition +* fix PartitionDefinition to Serializable +* --------- +* Co-authored-by: mj-db <mj.db@kakaocorp.com> [7b0e4](https://github.com/JSQLParser/JSqlParser/commit/7b0e42f60a60153) Minjae Lee *2024-11-12 12:05:28* **Add parsing functionality for MySQL CONVERT TO statement (#2097)** -* Co-authored-by: mj-db <mj.db@kakaocorp.com> +* Co-authored-by: mj-db <mj.db@kakaocorp.com> [dc123](https://github.com/JSQLParser/JSqlParser/commit/dc123820f869803) Minjae Lee *2024-10-25 15:33:05* **Fix issue 2090: Correctly parse LOCK clause in ALTER TABLE statements (#2095)** -* Add support for parsing LOCK clause in ALTER TABLE statements -* fix code formatting -* --------- -* Co-authored-by: mj-db <mj.db@kakaocorp.com> +* Add support for parsing LOCK clause in ALTER TABLE statements +* fix code formatting +* --------- +* Co-authored-by: mj-db <mj.db@kakaocorp.com> [f3f6b](https://github.com/JSQLParser/JSqlParser/commit/f3f6b72731d5c92) Minjae Lee *2024-10-24 12:20:58* **Fix parsing `ALTER TABLE ... ADD COLUMNS (...)` (#2087)** -* The current behaviour fails when it encounters `ADD COLUMNS` and merges -* everything in the `()` into one string with no whitespace. -* So `ALTER TABLE catalog.table.name ADD COLUMNS (apples int)` becomes -* `ALTER TABLE catalog.table.name ADD COLUMNS (applesint)`. -* This commit adds an understanding of how `ADD COLUMNS` to the grammar. +* The current behaviour fails when it encounters `ADD COLUMNS` and merges +* everything in the `()` into one string with no whitespace. +* So `ALTER TABLE catalog.table.name ADD COLUMNS (apples int)` becomes +* `ALTER TABLE catalog.table.name ADD COLUMNS (applesint)`. +* This commit adds an understanding of how `ADD COLUMNS` to the grammar. [22909](https://github.com/JSQLParser/JSqlParser/commit/229099c74b92845) Nathan Jaremko *2024-10-24 01:11:46* **Failure to parse query with PRIOR in select list (#2083)** -* Added support for PRIOR operator to be used in select list -* Fixed copy paste issue +* Added support for PRIOR operator to be used in select list +* Fixed copy paste issue [30469](https://github.com/JSQLParser/JSqlParser/commit/30469248935ecb9) hannes92 *2024-10-02 23:57:04* **Skyline syntax (preferring clause) (#2078)** -* feat: Implement skyline syntax (preferring clause) -* fix: Fix codacy errors -* style: Execute :spotlessApply -* fix: Remove unused import -* refactor: Replace wildcard imports -* refactor: Remove redundant imports -* --------- -* Co-authored-by: Stefan Steinhauser <stefan.steinhauser@arz.at> +* feat: Implement skyline syntax (preferring clause) +* fix: Fix codacy errors +* style: Execute :spotlessApply +* fix: Remove unused import +* refactor: Replace wildcard imports +* refactor: Remove redundant imports +* --------- +* Co-authored-by: Stefan Steinhauser <stefan.steinhauser@arz.at> [d1373](https://github.com/JSQLParser/JSqlParser/commit/d1373c5f7048224) Stefan Steinhauser *2024-09-17 09:31:32* **Unparenthesized `SubSelect` as `FromItem` (#2073)** -* feat: Implement FromItem in Select -* Implement FromItem interface in Select to allow Select being an unparenthesized FromItem -* Refs: JSQLParser/JSqlParser#2071 -* feat: Implement grammar to support Select as FromItem -* Refs: JSQLParser/JSqlParser#2071 -* style: Run gradle :spotlessApply -* style: Run gradle :spotlessApply -* --------- -* Co-authored-by: Stefan Steinhauser <stefan.steinhauser@arz.at> +* feat: Implement FromItem in Select +* Implement FromItem interface in Select to allow Select being an unparenthesized FromItem +* Refs: JSQLParser/JSqlParser#2071 +* feat: Implement grammar to support Select as FromItem +* Refs: JSQLParser/JSqlParser#2071 +* style: Run gradle :spotlessApply +* style: Run gradle :spotlessApply +* --------- +* Co-authored-by: Stefan Steinhauser <stefan.steinhauser@arz.at> [60f4d](https://github.com/JSQLParser/JSqlParser/commit/60f4d74f7d8d500) Stefan Steinhauser *2024-09-10 10:49:49* @@ -481,51 +1353,51 @@ Changelog of JSqlParser. **Exasol support (#2046)** -* feat: Support REGEXP_LIKE as LikeExpression -* Implement support of REGEXP_LIKE as LikeExpression as described here: https://docs.exasol.com/db/latest/sql_references/predicates/not_regexp_like.htm -* feat: Support sub select as part of function parameters -* Allow unparenthesesed sub selects being part of multiple function parameters -* fix: Readd mistakenly removed K_TEXT_LITERAL -* revert: Revert code formatting changes -* fix: Fix choice conflicts -* refactor: Rename test methods -* refactor: Apply on changed files -* feat: Introduce allowUnparenthesizedSubSelects feature -* refactor: Apply formatApply -* test: add test for the standard grammar, expected to fail -* test: make the performance tests more robust regarding the time-outs -* style: reformat code -* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> -* fix: Revert default keyword changes -* fix: Revert default keyword changes -* --------- -* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> -* Co-authored-by: Stefan Steinhauser <stefan.steinhauser@arz.at> -* Co-authored-by: Andreas Reichel <andreas@manticore-projects.com> +* feat: Support REGEXP_LIKE as LikeExpression +* Implement support of REGEXP_LIKE as LikeExpression as described here: https://docs.exasol.com/db/latest/sql_references/predicates/not_regexp_like.htm +* feat: Support sub select as part of function parameters +* Allow unparenthesesed sub selects being part of multiple function parameters +* fix: Readd mistakenly removed K_TEXT_LITERAL +* revert: Revert code formatting changes +* fix: Fix choice conflicts +* refactor: Rename test methods +* refactor: Apply on changed files +* feat: Introduce allowUnparenthesizedSubSelects feature +* refactor: Apply formatApply +* test: add test for the standard grammar, expected to fail +* test: make the performance tests more robust regarding the time-outs +* style: reformat code +* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> +* fix: Revert default keyword changes +* fix: Revert default keyword changes +* --------- +* Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> +* Co-authored-by: Stefan Steinhauser <stefan.steinhauser@arz.at> +* Co-authored-by: Andreas Reichel <andreas@manticore-projects.com> [93ca6](https://github.com/JSQLParser/JSqlParser/commit/93ca6610425fc8a) Stefan Steinhauser *2024-08-20 08:16:27* **chore removing system.out.println lines + minor clean up of unit test scripts (#2060)** -* chore removing system.out.println lines + minor clean up -* formatting fixes via spotlessApply +* chore removing system.out.println lines + minor clean up +* formatting fixes via spotlessApply [11616](https://github.com/JSQLParser/JSqlParser/commit/11616a0206ed7c5) nicky6s *2024-08-19 07:50:32* **feature/fix: parsing inserts/updates/delete within CTEs (#2055)** -* feature parsing inserts/updates/delete within CTEs -* removing System lines -* fixing codacy issues -* reducing the looping in NestedBracketsPerformanceTest to just 6 -* formatting fixes via spotlessApply +* feature parsing inserts/updates/delete within CTEs +* removing System lines +* fixing codacy issues +* reducing the looping in NestedBracketsPerformanceTest to just 6 +* formatting fixes via spotlessApply [82470](https://github.com/JSQLParser/JSqlParser/commit/82470e55aaf9157) nicky6s *2024-08-04 16:45:56* **chore adding extra details to unit test scenarios (#2051)** -* chore adding extra details to unit test scenarios -* addressing review comments +* chore adding extra details to unit test scenarios +* addressing review comments [21c60](https://github.com/JSQLParser/JSqlParser/commit/21c605e3e2f5f52) nicky6s *2024-07-31 19:19:08* @@ -536,32 +1408,32 @@ Changelog of JSqlParser. **fix truncate parsing to capture multiple tables (#2048)** -* fix truncate parsing to capture multiple tables -* followup fixes including adding a lookahead -* replacng tabs with spaces -* increasing lookahead -* fixing after getting maven working locally +* fix truncate parsing to capture multiple tables +* followup fixes including adding a lookahead +* replacng tabs with spaces +* increasing lookahead +* fixing after getting maven working locally [aca82](https://github.com/JSQLParser/JSqlParser/commit/aca82f5926b2e95) Nick Redfearn *2024-07-26 11:52:27* **Resolve parsing error for CHARACTER SET and COLLATE in MySQL ALTER TABLE (issue 2027) (#2045)** -* fix: parsing alter text character set -* style: remove unnecessary comment -* fix: TEXT as a data type token -* --------- -* Co-authored-by: mj-db <mj.db@kakaocorp.com> +* fix: parsing alter text character set +* style: remove unnecessary comment +* fix: TEXT as a data type token +* --------- +* Co-authored-by: mj-db <mj.db@kakaocorp.com> [93c82](https://github.com/JSQLParser/JSqlParser/commit/93c8210e218d7f5) Minjae Lee *2024-07-23 02:27:05* **Avoid private tokens to be white listed and allow any word character in token value (#2044)** -* fix: Avoid private tokens to be white listed -* Closes JSQLParser/JSqlParser#2040 -* feat: Allow all word characters as token value -* Closes JSQLParser/JSqlParser#2041 -* --------- -* Co-authored-by: Stefan Steinhauser <stefan.steinhauser@arz.at> +* fix: Avoid private tokens to be white listed +* Closes JSQLParser/JSqlParser#2040 +* feat: Allow all word characters as token value +* Closes JSQLParser/JSqlParser#2041 +* --------- +* Co-authored-by: Stefan Steinhauser <stefan.steinhauser@arz.at> [821e9](https://github.com/JSQLParser/JSqlParser/commit/821e92e8fb6e48e) Stefan Steinhauser *2024-07-20 10:35:47* @@ -570,97 +1442,97 @@ Changelog of JSqlParser. ### Breaking changes -- Visitors return Objects and accept parameters ([5bd28](https://github.com/JSQLParser/JSqlParser/commit/5bd28c8b309df6c) Andreas Reichel) -- Visitors return Objects ([131a9](https://github.com/JSQLParser/JSqlParser/commit/131a988ccea2d91) Andreas Reichel) -- Visitors return Objects ([c2328](https://github.com/JSQLParser/JSqlParser/commit/c2328120e7a79ff) Andreas Reichel) -- Visitors return Objects ([ec497](https://github.com/JSQLParser/JSqlParser/commit/ec49762708e920a) Andreas Reichel) -- Visitors return Objects ([681ca](https://github.com/JSQLParser/JSqlParser/commit/681cac933d83516) Andreas Reichel) +- Visitors return Objects and accept parameters ([5bd28](https://github.com/JSQLParser/JSqlParser/commit/5bd28c8b309df6c) Andreas Reichel) +- Visitors return Objects ([131a9](https://github.com/JSQLParser/JSqlParser/commit/131a988ccea2d91) Andreas Reichel) +- Visitors return Objects ([c2328](https://github.com/JSQLParser/JSqlParser/commit/c2328120e7a79ff) Andreas Reichel) +- Visitors return Objects ([ec497](https://github.com/JSQLParser/JSqlParser/commit/ec49762708e920a) Andreas Reichel) +- Visitors return Objects ([681ca](https://github.com/JSQLParser/JSqlParser/commit/681cac933d83516) Andreas Reichel) ### Features -- provide compatibility methods ([3f995](https://github.com/JSQLParser/JSqlParser/commit/3f99548b99bbfe3) Andreas Reichel) -- apply the new parametrized Visitor patterns to all entities and provide default implementations ([e1692](https://github.com/JSQLParser/JSqlParser/commit/e1692990c543ed1) Andreas Reichel) -- syntax sugar ([2fce4](https://github.com/JSQLParser/JSqlParser/commit/2fce4c009b77d85) Andreas Reichel) -- Visitors return Objects and accept parameters ([5bd28](https://github.com/JSQLParser/JSqlParser/commit/5bd28c8b309df6c) Andreas Reichel) -- Visitors return Objects ([131a9](https://github.com/JSQLParser/JSqlParser/commit/131a988ccea2d91) Andreas Reichel) -- Visitors return Objects ([c2328](https://github.com/JSQLParser/JSqlParser/commit/c2328120e7a79ff) Andreas Reichel) -- Visitors return Objects ([ec497](https://github.com/JSQLParser/JSqlParser/commit/ec49762708e920a) Andreas Reichel) -- Visitors return Objects ([681ca](https://github.com/JSQLParser/JSqlParser/commit/681cac933d83516) Andreas Reichel) -- Allow OUTER keyword as function parameter name (#2021) ([fc90c](https://github.com/JSQLParser/JSqlParser/commit/fc90c0b5e566533) Chris Crabtree) -- BigQuery `SELECT AS STRUCT ...` and `SELECT AS VALUE ...` ([5c360](https://github.com/JSQLParser/JSqlParser/commit/5c360a2fc95c261) Andreas Reichel) -- add syntax sugar ([2ace7](https://github.com/JSQLParser/JSqlParser/commit/2ace74d1047e87d) Andreas Reichel) -- `AllColumns`, DuckDB uses `EXCLUDE` instead of `EXCEPT` ([1ad42](https://github.com/JSQLParser/JSqlParser/commit/1ad4234280f7a70) Andreas Reichel) -- syntax sugar ([ae1ef](https://github.com/JSQLParser/JSqlParser/commit/ae1eff9f7434c08) Andreas Reichel) -- syntax sugar ([81846](https://github.com/JSQLParser/JSqlParser/commit/818464c93ae665a) Andreas Reichel) -- syntax sugar ([2cb3e](https://github.com/JSQLParser/JSqlParser/commit/2cb3e589b60e192) Andreas Reichel) -- syntax sugar ([b2eed](https://github.com/JSQLParser/JSqlParser/commit/b2eed1e910c97de) Andreas Reichel) -- Databricks IGNORE/RESPECT NULLS ([e9c9a](https://github.com/JSQLParser/JSqlParser/commit/e9c9a173a660bbe) Andreas Reichel) -- Databricks IGNORE/RESPECT NULLS ([544b1](https://github.com/JSQLParser/JSqlParser/commit/544b1683789f20b) Andreas Reichel) -- Capture expression name part delimiters (#2001) ([0368b](https://github.com/JSQLParser/JSqlParser/commit/0368b9ebad76742) Chris Crabtree) -- syntax sugar ([ca5c5](https://github.com/JSQLParser/JSqlParser/commit/ca5c553efde37eb) Andreas Reichel) -- translate HEX to Unicode String and ByteArray String ([df519](https://github.com/JSQLParser/JSqlParser/commit/df519333ff34740) Andreas Reichel) -- `StructType` syntax sugar ([6e9bf](https://github.com/JSQLParser/JSqlParser/commit/6e9bf42b0b2d783) Andreas Reichel) -- `Values` implement `FromItem` ([e426c](https://github.com/JSQLParser/JSqlParser/commit/e426c5a67c505b5) Andreas Reichel) -- add `ParenthesedSelect` delegate ([66d05](https://github.com/JSQLParser/JSqlParser/commit/66d05a2bb7c41f3) Andreas Reichel) -- add `ParenthesedSelect` delegate ([f1699](https://github.com/JSQLParser/JSqlParser/commit/f16999393589702) Andreas Reichel) -- Simplify traversing the AST bottom to top ([bddc4](https://github.com/JSQLParser/JSqlParser/commit/bddc41cddf5b5bf) Andreas Reichel) -- AST Node access for `FromItem` ([c1edf](https://github.com/JSQLParser/JSqlParser/commit/c1edf0f8f21bd52) Andreas Reichel) -- RedShift specific Window function IGNORE | RESPECT NULLS ([321c8](https://github.com/JSQLParser/JSqlParser/commit/321c88098a75791) Andreas Reichel) -- RedShift allows `TOP` before `DISTINCT`, see https://docs.aws.amazon.com/redshift/latest/dg/r_SELECT_list.html ([13e61](https://github.com/JSQLParser/JSqlParser/commit/13e61a726a87c2f) Andreas Reichel) -- Redshift `APPROXIMATE` Aggregate functions ([e4ece](https://github.com/JSQLParser/JSqlParser/commit/e4ece0c3ecd7ce3) Andreas Reichel) -- add `CCJSqlParserUtil.sanitizeSingleSql(String sqlStr)` to help MyBatikPlus users to clean their statements ([1606e](https://github.com/JSQLParser/JSqlParser/commit/1606e5f0492a485) Andreas Reichel) -- return any `UnsupportedStatement` content ([063d2](https://github.com/JSQLParser/JSqlParser/commit/063d2442d82f920) Andreas Reichel) -- re-enable `UnsupportedStatement` ([82b45](https://github.com/JSQLParser/JSqlParser/commit/82b459bfcd23851) Andreas Reichel) -- better statement error recovery ([b3d3a](https://github.com/JSQLParser/JSqlParser/commit/b3d3a8e492f74a8) Andreas Reichel) -- Syntax Sugar for the parser features ([1d943](https://github.com/JSQLParser/JSqlParser/commit/1d9438e7ef1a86f) Andreas Reichel) -- allow `EXTRACT` to be parsed as regular function also ([b85dc](https://github.com/JSQLParser/JSqlParser/commit/b85dc2fd0004652) Andreas Reichel) -- syntax sugar ([a3858](https://github.com/JSQLParser/JSqlParser/commit/a38581acd538d95) Andreas Reichel) -- syntax sugar ([df7c7](https://github.com/JSQLParser/JSqlParser/commit/df7c792184c61a6) Andreas Reichel) -- Syntax sugar ([67bfa](https://github.com/JSQLParser/JSqlParser/commit/67bfae673421d7c) Andreas Reichel) -- syntax sugar ([b0317](https://github.com/JSQLParser/JSqlParser/commit/b03170e180175b1) Andreas Reichel) -- syntax sugar ([57a29](https://github.com/JSQLParser/JSqlParser/commit/57a296b2c8c5bb0) Andreas Reichel) -- remove Aliases of `ParenthesedSelect`, `LateralSubSelect` and `ParenthesedFromItem` from the Table Names ([46682](https://github.com/JSQLParser/JSqlParser/commit/466826b9b115cb7) Andreas Reichel) -- better access to the `DataType` checks ([edeaf](https://github.com/JSQLParser/JSqlParser/commit/edeafc311c2ab7e) Andreas Reichel) -- Add Data Type information to task for making it easy to understand the expected return type ([31c55](https://github.com/JSQLParser/JSqlParser/commit/31c5533f49776c6) Andreas Reichel) -- Implicit Casts `SELECT DOUBLE PRECISION '1'` ([411a3](https://github.com/JSQLParser/JSqlParser/commit/411a3da9facf206) Andreas Reichel) -- Function Column Aliases without an Alias Name `func(x) (a, b, c)` ([b4ef7](https://github.com/JSQLParser/JSqlParser/commit/b4ef763614bf3a4) Andreas Reichel) -- Support BigQuery specific Aggregate clauses ([0179c](https://github.com/JSQLParser/JSqlParser/commit/0179cc0cac9ceeb) Andreas Reichel) -- syntax sugar for Binary Expressions like Conact, Addition, Multiplication ([ffdde](https://github.com/JSQLParser/JSqlParser/commit/ffddeef7199a056) Andreas Reichel) -- Hex to Long conversion ([620db](https://github.com/JSQLParser/JSqlParser/commit/620db709e48c22e) Andreas Reichel) -- syntax sugar for Expressions ([a5693](https://github.com/JSQLParser/JSqlParser/commit/a56934da1d3a7ae) Andreas Reichel) -- Salesforce SOQL `INCLUDES` and `EXCLUDES` operators (#1985) ([f3f0e](https://github.com/JSQLParser/JSqlParser/commit/f3f0e051358a493) lucarota) -- Google BigQuery `CAST` with `FORMAT` clause ([0d813](https://github.com/JSQLParser/JSqlParser/commit/0d813f03faa2b3b) Andreas Reichel) -- DuckDB Lambda Functions ([23679](https://github.com/JSQLParser/JSqlParser/commit/236793aaeabc30f) Andreas Reichel) -- DuckDB `STRUCT` with curly brackets and explicit Column Type Cast ([1cd57](https://github.com/JSQLParser/JSqlParser/commit/1cd576b32c774e8) Andreas Reichel) -- `RECURSIVE` does not need to be a reserved ([5cb4c](https://github.com/JSQLParser/JSqlParser/commit/5cb4c55067f4fe2) Andreas Reichel) -- DuckDB `STRUCT` with curly brackets ([339d6](https://github.com/JSQLParser/JSqlParser/commit/339d6baece2c199) Andreas Reichel) -- BigQuery `STRUCT` data types and literal ([4c187](https://github.com/JSQLParser/JSqlParser/commit/4c187d51055a3d8) Andreas Reichel) -- TablesNamesFinder can return also references to WITH items ([9d645](https://github.com/JSQLParser/JSqlParser/commit/9d64511239a4514) Andreas Reichel) -- allow double-quoted `DateTimeLiteral` like `DATETIME "2005-01-03 12:34:56"` ([f6790](https://github.com/JSQLParser/JSqlParser/commit/f6790913b754850) Andreas Reichel) -- support `DATETIME` literal used for Google BigQuery ([a386d](https://github.com/JSQLParser/JSqlParser/commit/a386d297c418921) Andreas Reichel) -- link `TOP` to AST node ([79c42](https://github.com/JSQLParser/JSqlParser/commit/79c42ed31eb6986) Andreas Reichel) +- provide compatibility methods ([3f995](https://github.com/JSQLParser/JSqlParser/commit/3f99548b99bbfe3) Andreas Reichel) +- apply the new parametrized Visitor patterns to all entities and provide default implementations ([e1692](https://github.com/JSQLParser/JSqlParser/commit/e1692990c543ed1) Andreas Reichel) +- syntax sugar ([2fce4](https://github.com/JSQLParser/JSqlParser/commit/2fce4c009b77d85) Andreas Reichel) +- Visitors return Objects and accept parameters ([5bd28](https://github.com/JSQLParser/JSqlParser/commit/5bd28c8b309df6c) Andreas Reichel) +- Visitors return Objects ([131a9](https://github.com/JSQLParser/JSqlParser/commit/131a988ccea2d91) Andreas Reichel) +- Visitors return Objects ([c2328](https://github.com/JSQLParser/JSqlParser/commit/c2328120e7a79ff) Andreas Reichel) +- Visitors return Objects ([ec497](https://github.com/JSQLParser/JSqlParser/commit/ec49762708e920a) Andreas Reichel) +- Visitors return Objects ([681ca](https://github.com/JSQLParser/JSqlParser/commit/681cac933d83516) Andreas Reichel) +- Allow OUTER keyword as function parameter name (#2021) ([fc90c](https://github.com/JSQLParser/JSqlParser/commit/fc90c0b5e566533) Chris Crabtree) +- BigQuery `SELECT AS STRUCT ...` and `SELECT AS VALUE ...` ([5c360](https://github.com/JSQLParser/JSqlParser/commit/5c360a2fc95c261) Andreas Reichel) +- add syntax sugar ([2ace7](https://github.com/JSQLParser/JSqlParser/commit/2ace74d1047e87d) Andreas Reichel) +- `AllColumns`, DuckDB uses `EXCLUDE` instead of `EXCEPT` ([1ad42](https://github.com/JSQLParser/JSqlParser/commit/1ad4234280f7a70) Andreas Reichel) +- syntax sugar ([ae1ef](https://github.com/JSQLParser/JSqlParser/commit/ae1eff9f7434c08) Andreas Reichel) +- syntax sugar ([81846](https://github.com/JSQLParser/JSqlParser/commit/818464c93ae665a) Andreas Reichel) +- syntax sugar ([2cb3e](https://github.com/JSQLParser/JSqlParser/commit/2cb3e589b60e192) Andreas Reichel) +- syntax sugar ([b2eed](https://github.com/JSQLParser/JSqlParser/commit/b2eed1e910c97de) Andreas Reichel) +- Databricks IGNORE/RESPECT NULLS ([e9c9a](https://github.com/JSQLParser/JSqlParser/commit/e9c9a173a660bbe) Andreas Reichel) +- Databricks IGNORE/RESPECT NULLS ([544b1](https://github.com/JSQLParser/JSqlParser/commit/544b1683789f20b) Andreas Reichel) +- Capture expression name part delimiters (#2001) ([0368b](https://github.com/JSQLParser/JSqlParser/commit/0368b9ebad76742) Chris Crabtree) +- syntax sugar ([ca5c5](https://github.com/JSQLParser/JSqlParser/commit/ca5c553efde37eb) Andreas Reichel) +- translate HEX to Unicode String and ByteArray String ([df519](https://github.com/JSQLParser/JSqlParser/commit/df519333ff34740) Andreas Reichel) +- `StructType` syntax sugar ([6e9bf](https://github.com/JSQLParser/JSqlParser/commit/6e9bf42b0b2d783) Andreas Reichel) +- `Values` implement `FromItem` ([e426c](https://github.com/JSQLParser/JSqlParser/commit/e426c5a67c505b5) Andreas Reichel) +- add `ParenthesedSelect` delegate ([66d05](https://github.com/JSQLParser/JSqlParser/commit/66d05a2bb7c41f3) Andreas Reichel) +- add `ParenthesedSelect` delegate ([f1699](https://github.com/JSQLParser/JSqlParser/commit/f16999393589702) Andreas Reichel) +- Simplify traversing the AST bottom to top ([bddc4](https://github.com/JSQLParser/JSqlParser/commit/bddc41cddf5b5bf) Andreas Reichel) +- AST Node access for `FromItem` ([c1edf](https://github.com/JSQLParser/JSqlParser/commit/c1edf0f8f21bd52) Andreas Reichel) +- RedShift specific Window function IGNORE | RESPECT NULLS ([321c8](https://github.com/JSQLParser/JSqlParser/commit/321c88098a75791) Andreas Reichel) +- RedShift allows `TOP` before `DISTINCT`, see https://docs.aws.amazon.com/redshift/latest/dg/r_SELECT_list.html ([13e61](https://github.com/JSQLParser/JSqlParser/commit/13e61a726a87c2f) Andreas Reichel) +- Redshift `APPROXIMATE` Aggregate functions ([e4ece](https://github.com/JSQLParser/JSqlParser/commit/e4ece0c3ecd7ce3) Andreas Reichel) +- add `CCJSqlParserUtil.sanitizeSingleSql(String sqlStr)` to help MyBatikPlus users to clean their statements ([1606e](https://github.com/JSQLParser/JSqlParser/commit/1606e5f0492a485) Andreas Reichel) +- return any `UnsupportedStatement` content ([063d2](https://github.com/JSQLParser/JSqlParser/commit/063d2442d82f920) Andreas Reichel) +- re-enable `UnsupportedStatement` ([82b45](https://github.com/JSQLParser/JSqlParser/commit/82b459bfcd23851) Andreas Reichel) +- better statement error recovery ([b3d3a](https://github.com/JSQLParser/JSqlParser/commit/b3d3a8e492f74a8) Andreas Reichel) +- Syntax Sugar for the parser features ([1d943](https://github.com/JSQLParser/JSqlParser/commit/1d9438e7ef1a86f) Andreas Reichel) +- allow `EXTRACT` to be parsed as regular function also ([b85dc](https://github.com/JSQLParser/JSqlParser/commit/b85dc2fd0004652) Andreas Reichel) +- syntax sugar ([a3858](https://github.com/JSQLParser/JSqlParser/commit/a38581acd538d95) Andreas Reichel) +- syntax sugar ([df7c7](https://github.com/JSQLParser/JSqlParser/commit/df7c792184c61a6) Andreas Reichel) +- Syntax sugar ([67bfa](https://github.com/JSQLParser/JSqlParser/commit/67bfae673421d7c) Andreas Reichel) +- syntax sugar ([b0317](https://github.com/JSQLParser/JSqlParser/commit/b03170e180175b1) Andreas Reichel) +- syntax sugar ([57a29](https://github.com/JSQLParser/JSqlParser/commit/57a296b2c8c5bb0) Andreas Reichel) +- remove Aliases of `ParenthesedSelect`, `LateralSubSelect` and `ParenthesedFromItem` from the Table Names ([46682](https://github.com/JSQLParser/JSqlParser/commit/466826b9b115cb7) Andreas Reichel) +- better access to the `DataType` checks ([edeaf](https://github.com/JSQLParser/JSqlParser/commit/edeafc311c2ab7e) Andreas Reichel) +- Add Data Type information to task for making it easy to understand the expected return type ([31c55](https://github.com/JSQLParser/JSqlParser/commit/31c5533f49776c6) Andreas Reichel) +- Implicit Casts `SELECT DOUBLE PRECISION '1'` ([411a3](https://github.com/JSQLParser/JSqlParser/commit/411a3da9facf206) Andreas Reichel) +- Function Column Aliases without an Alias Name `func(x) (a, b, c)` ([b4ef7](https://github.com/JSQLParser/JSqlParser/commit/b4ef763614bf3a4) Andreas Reichel) +- Support BigQuery specific Aggregate clauses ([0179c](https://github.com/JSQLParser/JSqlParser/commit/0179cc0cac9ceeb) Andreas Reichel) +- syntax sugar for Binary Expressions like Conact, Addition, Multiplication ([ffdde](https://github.com/JSQLParser/JSqlParser/commit/ffddeef7199a056) Andreas Reichel) +- Hex to Long conversion ([620db](https://github.com/JSQLParser/JSqlParser/commit/620db709e48c22e) Andreas Reichel) +- syntax sugar for Expressions ([a5693](https://github.com/JSQLParser/JSqlParser/commit/a56934da1d3a7ae) Andreas Reichel) +- Salesforce SOQL `INCLUDES` and `EXCLUDES` operators (#1985) ([f3f0e](https://github.com/JSQLParser/JSqlParser/commit/f3f0e051358a493) lucarota) +- Google BigQuery `CAST` with `FORMAT` clause ([0d813](https://github.com/JSQLParser/JSqlParser/commit/0d813f03faa2b3b) Andreas Reichel) +- DuckDB Lambda Functions ([23679](https://github.com/JSQLParser/JSqlParser/commit/236793aaeabc30f) Andreas Reichel) +- DuckDB `STRUCT` with curly brackets and explicit Column Type Cast ([1cd57](https://github.com/JSQLParser/JSqlParser/commit/1cd576b32c774e8) Andreas Reichel) +- `RECURSIVE` does not need to be a reserved ([5cb4c](https://github.com/JSQLParser/JSqlParser/commit/5cb4c55067f4fe2) Andreas Reichel) +- DuckDB `STRUCT` with curly brackets ([339d6](https://github.com/JSQLParser/JSqlParser/commit/339d6baece2c199) Andreas Reichel) +- BigQuery `STRUCT` data types and literal ([4c187](https://github.com/JSQLParser/JSqlParser/commit/4c187d51055a3d8) Andreas Reichel) +- TablesNamesFinder can return also references to WITH items ([9d645](https://github.com/JSQLParser/JSqlParser/commit/9d64511239a4514) Andreas Reichel) +- allow double-quoted `DateTimeLiteral` like `DATETIME "2005-01-03 12:34:56"` ([f6790](https://github.com/JSQLParser/JSqlParser/commit/f6790913b754850) Andreas Reichel) +- support `DATETIME` literal used for Google BigQuery ([a386d](https://github.com/JSQLParser/JSqlParser/commit/a386d297c418921) Andreas Reichel) +- link `TOP` to AST node ([79c42](https://github.com/JSQLParser/JSqlParser/commit/79c42ed31eb6986) Andreas Reichel) ### Bug Fixes -- `AllTableColumns`, DuckDB specific `EXCLUDE` ([c9ecf](https://github.com/JSQLParser/JSqlParser/commit/c9ecfc6ddbdd139) Andreas Reichel) -- `AllColumns` Replacement shall be about Columns only ([f4b40](https://github.com/JSQLParser/JSqlParser/commit/f4b40e43a4f8d3d) Andreas Reichel) -- `FromItem` with Alias without `AS` keyword ([5f580](https://github.com/JSQLParser/JSqlParser/commit/5f580af190c6fbb) Andreas Reichel) -- set `stringValue` in `DoubleValue.setValue` (#2009) ([e07f8](https://github.com/JSQLParser/JSqlParser/commit/e07f8d019ddf38d) Damian) -- try working around `UnsupportedStatement` issue ([fbe97](https://github.com/JSQLParser/JSqlParser/commit/fbe97a8deb84ae9) Andreas Reichel) -- allow `BASE64` keyword ([7daf7](https://github.com/JSQLParser/JSqlParser/commit/7daf7af36d825f7) Andreas Reichel) -- `StructType` expressions must use Visitor instead of `toString()` ([b95d8](https://github.com/JSQLParser/JSqlParser/commit/b95d8e3e4ee01b0) Andreas Reichel) -- `AnyComparisionItem` with extra brackets ([4e1a1](https://github.com/JSQLParser/JSqlParser/commit/4e1a1535f4ef706) Andreas Reichel) -- `FOR UPDATE` clause should come after the select body ([cf7fe](https://github.com/JSQLParser/JSqlParser/commit/cf7fe157de372f3) Andreas Reichel) -- initialise the `SelectDeparser` with an `ExpressionDeparser` (but not with an empty Adaptor only) ([f417c](https://github.com/JSQLParser/JSqlParser/commit/f417c8f248c7bb1) Andreas Reichel) -- `ALTER ...` shall `captureRest()` only to the next statement terminator ([15d14](https://github.com/JSQLParser/JSqlParser/commit/15d14ab0b9dcadf) Andreas Reichel) -- correct the wrong Assertion ([8461e](https://github.com/JSQLParser/JSqlParser/commit/8461e8ad1a3f5e3) Andreas Reichel) -- don't insert space after certain punctuation ([159c2](https://github.com/JSQLParser/JSqlParser/commit/159c28ee8f68cab) Andreas Reichel) -- treat Array Brackets `[..]` as syntax characters and surround by space when normalizing for comparison ([c9d1e](https://github.com/JSQLParser/JSqlParser/commit/c9d1eaefca91c6e) Andreas Reichel) -- `REGEXP` does not need to be reserved ([f6524](https://github.com/JSQLParser/JSqlParser/commit/f65240f381f9855) Andreas Reichel) -- `REGEXP` does not need to be reserved ([a9e67](https://github.com/JSQLParser/JSqlParser/commit/a9e67667b9c1590) Andreas Reichel) -- Array Arguments without `ARRAY` keyword ([0f9a8](https://github.com/JSQLParser/JSqlParser/commit/0f9a8ec02786f5d) Andreas Reichel) -- Function with Array Arguments ([f782e](https://github.com/JSQLParser/JSqlParser/commit/f782eda7afa17d3) Andreas Reichel) -- parsing `SelectItem` shall support `Xor` ([c8839](https://github.com/JSQLParser/JSqlParser/commit/c883920a1175ffc) Andreas Reichel) +- `AllTableColumns`, DuckDB specific `EXCLUDE` ([c9ecf](https://github.com/JSQLParser/JSqlParser/commit/c9ecfc6ddbdd139) Andreas Reichel) +- `AllColumns` Replacement shall be about Columns only ([f4b40](https://github.com/JSQLParser/JSqlParser/commit/f4b40e43a4f8d3d) Andreas Reichel) +- `FromItem` with Alias without `AS` keyword ([5f580](https://github.com/JSQLParser/JSqlParser/commit/5f580af190c6fbb) Andreas Reichel) +- set `stringValue` in `DoubleValue.setValue` (#2009) ([e07f8](https://github.com/JSQLParser/JSqlParser/commit/e07f8d019ddf38d) Damian) +- try working around `UnsupportedStatement` issue ([fbe97](https://github.com/JSQLParser/JSqlParser/commit/fbe97a8deb84ae9) Andreas Reichel) +- allow `BASE64` keyword ([7daf7](https://github.com/JSQLParser/JSqlParser/commit/7daf7af36d825f7) Andreas Reichel) +- `StructType` expressions must use Visitor instead of `toString()` ([b95d8](https://github.com/JSQLParser/JSqlParser/commit/b95d8e3e4ee01b0) Andreas Reichel) +- `AnyComparisionItem` with extra brackets ([4e1a1](https://github.com/JSQLParser/JSqlParser/commit/4e1a1535f4ef706) Andreas Reichel) +- `FOR UPDATE` clause should come after the select body ([cf7fe](https://github.com/JSQLParser/JSqlParser/commit/cf7fe157de372f3) Andreas Reichel) +- initialise the `SelectDeparser` with an `ExpressionDeparser` (but not with an empty Adaptor only) ([f417c](https://github.com/JSQLParser/JSqlParser/commit/f417c8f248c7bb1) Andreas Reichel) +- `ALTER ...` shall `captureRest()` only to the next statement terminator ([15d14](https://github.com/JSQLParser/JSqlParser/commit/15d14ab0b9dcadf) Andreas Reichel) +- correct the wrong Assertion ([8461e](https://github.com/JSQLParser/JSqlParser/commit/8461e8ad1a3f5e3) Andreas Reichel) +- don't insert space after certain punctuation ([159c2](https://github.com/JSQLParser/JSqlParser/commit/159c28ee8f68cab) Andreas Reichel) +- treat Array Brackets `[..]` as syntax characters and surround by space when normalizing for comparison ([c9d1e](https://github.com/JSQLParser/JSqlParser/commit/c9d1eaefca91c6e) Andreas Reichel) +- `REGEXP` does not need to be reserved ([f6524](https://github.com/JSQLParser/JSqlParser/commit/f65240f381f9855) Andreas Reichel) +- `REGEXP` does not need to be reserved ([a9e67](https://github.com/JSQLParser/JSqlParser/commit/a9e67667b9c1590) Andreas Reichel) +- Array Arguments without `ARRAY` keyword ([0f9a8](https://github.com/JSQLParser/JSqlParser/commit/0f9a8ec02786f5d) Andreas Reichel) +- Function with Array Arguments ([f782e](https://github.com/JSQLParser/JSqlParser/commit/f782eda7afa17d3) Andreas Reichel) +- parsing `SelectItem` shall support `Xor` ([c8839](https://github.com/JSQLParser/JSqlParser/commit/c883920a1175ffc) Andreas Reichel) ### Other changes @@ -686,18 +1558,18 @@ Changelog of JSqlParser. **Add missing java.sql require (#1999)** -* Add missing java.sql -* Update maven checkstyle -* Fix gradle checkstyle -* Bump surefire plugin -* Skip modules in tests +* Add missing java.sql +* Update maven checkstyle +* Fix gradle checkstyle +* Bump surefire plugin +* Skip modules in tests [df48c](https://github.com/JSQLParser/JSqlParser/commit/df48c4ba5b2b44f) Ethan McCue *2024-04-30 05:13:54* **Add module info (#1998)** -* Add module info -* Trailing newline +* Add module info +* Trailing newline [761b4](https://github.com/JSQLParser/JSqlParser/commit/761b45b2f6c4b81) Ethan McCue *2024-04-30 04:36:41* @@ -711,24 +1583,24 @@ Changelog of JSqlParser. ### Features -- add DB2 special register `CURRENT TIMEZONE` ([c412d](https://github.com/JSQLParser/JSqlParser/commit/c412d6a52f9b2ea) Andreas Reichel) -- add additional CREATE VIEW modifiers (#1964) ([67e22](https://github.com/JSQLParser/JSqlParser/commit/67e220425f24148) David Goss) -- with no log (#1953) ([d9c44](https://github.com/JSQLParser/JSqlParser/commit/d9c44499d096b1f) mjh) -- support keyword "only" for postgresql (#1952) ([f1676](https://github.com/JSQLParser/JSqlParser/commit/f1676dd992911d9) 猫屎咖啡) -- support any number/order of merge operations (#1938) ([f1c52](https://github.com/JSQLParser/JSqlParser/commit/f1c525a1eaf3087) David Goss) +- add DB2 special register `CURRENT TIMEZONE` ([c412d](https://github.com/JSQLParser/JSqlParser/commit/c412d6a52f9b2ea) Andreas Reichel) +- add additional CREATE VIEW modifiers (#1964) ([67e22](https://github.com/JSQLParser/JSqlParser/commit/67e220425f24148) David Goss) +- with no log (#1953) ([d9c44](https://github.com/JSQLParser/JSqlParser/commit/d9c44499d096b1f) mjh) +- support keyword "only" for postgresql (#1952) ([f1676](https://github.com/JSQLParser/JSqlParser/commit/f1676dd992911d9) 猫屎咖啡) +- support any number/order of merge operations (#1938) ([f1c52](https://github.com/JSQLParser/JSqlParser/commit/f1c525a1eaf3087) David Goss) ### Bug Fixes -- chained function calls of `SimpleFunction` ([98055](https://github.com/JSQLParser/JSqlParser/commit/9805581accf89d2) Andreas Reichel) -- issue #1948 `Between` with expression ([b9453](https://github.com/JSQLParser/JSqlParser/commit/b9453f228adf9ad) Andreas Reichel) -- return NULL when parsing empty Strings ([94fb8](https://github.com/JSQLParser/JSqlParser/commit/94fb87237f36cce) Andreas Reichel) -- allow Parameters like `$1`,`$2` ([17f5f](https://github.com/JSQLParser/JSqlParser/commit/17f5f2ad680dfdb) Andreas Reichel) -- allow `DATA` as `ColumnType()` keyword ([72a51](https://github.com/JSQLParser/JSqlParser/commit/72a51e58413a291) Andreas Reichel) -- make analytic expression visitor null-safe (#1944) ([768c6](https://github.com/JSQLParser/JSqlParser/commit/768c63f4660509b) David Goss) -- Fixes parsing failing for ALTER MODIFY queries not containing datatype (#1961) ([029fd](https://github.com/JSQLParser/JSqlParser/commit/029fd42e84e65ee) Tanish Grover) -- tables not find in parentheses join sql. (#1956) ([182f4](https://github.com/JSQLParser/JSqlParser/commit/182f484dc43945b) hancher) -- issue1875 (#1957) ([98aa9](https://github.com/JSQLParser/JSqlParser/commit/98aa90cb988580a) mjh) -- ExpressionVisitor.visit(AllTableColumns) method isn't being called. (#1942) ([bc166](https://github.com/JSQLParser/JSqlParser/commit/bc16618eaa8fd93) Brian S. O'Neill) +- chained function calls of `SimpleFunction` ([98055](https://github.com/JSQLParser/JSqlParser/commit/9805581accf89d2) Andreas Reichel) +- issue #1948 `Between` with expression ([b9453](https://github.com/JSQLParser/JSqlParser/commit/b9453f228adf9ad) Andreas Reichel) +- return NULL when parsing empty Strings ([94fb8](https://github.com/JSQLParser/JSqlParser/commit/94fb87237f36cce) Andreas Reichel) +- allow Parameters like `$1`,`$2` ([17f5f](https://github.com/JSQLParser/JSqlParser/commit/17f5f2ad680dfdb) Andreas Reichel) +- allow `DATA` as `ColumnType()` keyword ([72a51](https://github.com/JSQLParser/JSqlParser/commit/72a51e58413a291) Andreas Reichel) +- make analytic expression visitor null-safe (#1944) ([768c6](https://github.com/JSQLParser/JSqlParser/commit/768c63f4660509b) David Goss) +- Fixes parsing failing for ALTER MODIFY queries not containing datatype (#1961) ([029fd](https://github.com/JSQLParser/JSqlParser/commit/029fd42e84e65ee) Tanish Grover) +- tables not find in parentheses join sql. (#1956) ([182f4](https://github.com/JSQLParser/JSqlParser/commit/182f484dc43945b) hancher) +- issue1875 (#1957) ([98aa9](https://github.com/JSQLParser/JSqlParser/commit/98aa90cb988580a) mjh) +- ExpressionVisitor.visit(AllTableColumns) method isn't being called. (#1942) ([bc166](https://github.com/JSQLParser/JSqlParser/commit/bc16618eaa8fd93) Brian S. O'Neill) ### Other changes @@ -744,33 +1616,33 @@ Changelog of JSqlParser. **Update README.md** -* Fixes #1968 +* Fixes #1968 [8dcfb](https://github.com/JSQLParser/JSqlParser/commit/8dcfb4a3bf5682d) manticore-projects *2024-02-17 12:03:43* **Guard Values against null/empty values (#1965)** -* Guard Values against null/empty values -* The classes modified by this commit are `DoubleValue`, `LongValue`, and -* `TimeValue`. Both `null` and empty strings provided to their -* constructors fail, but they provide very different error messages -* (NullPointerException and StringIndexOutOfBoundsException), which is -* neither sensible nor helpful in debugging. -* This commit adds a guard to throw `IllegalArgumentException` for both -* cases in order to improve coherency and usefulness of the error -* messages. -* fix checkstyle issues +* Guard Values against null/empty values +* The classes modified by this commit are `DoubleValue`, `LongValue`, and +* `TimeValue`. Both `null` and empty strings provided to their +* constructors fail, but they provide very different error messages +* (NullPointerException and StringIndexOutOfBoundsException), which is +* neither sensible nor helpful in debugging. +* This commit adds a guard to throw `IllegalArgumentException` for both +* cases in order to improve coherency and usefulness of the error +* messages. +* fix checkstyle issues [b0032](https://github.com/JSQLParser/JSqlParser/commit/b00322efa0c77d2) Heewon Lee *2024-02-14 07:34:40* **support oracle alter table truncate partition (#1954)** -* feat: oracle alter table truncate partition -* feat: oracle alter table truncate partition -* feat: code format -* feat: code format -* --------- -* Co-authored-by: mjh <majh118@chinaunicom.cn> +* feat: oracle alter table truncate partition +* feat: oracle alter table truncate partition +* feat: code format +* feat: code format +* --------- +* Co-authored-by: mjh <majh118@chinaunicom.cn> [cc7aa](https://github.com/JSQLParser/JSqlParser/commit/cc7aa01913a7201) mjh *2024-02-04 07:19:19* @@ -804,35 +1676,35 @@ Changelog of JSqlParser. ### Features -- support mysql with rollup (#1923) ([77f6f](https://github.com/JSQLParser/JSqlParser/commit/77f6fb8c92b3378) jxnu-liguobin) -- Support `FOR SHARE` (#1922) ([815f8](https://github.com/JSQLParser/JSqlParser/commit/815f8753d552d89) jxnu-liguobin) -- [MySQL] Support `TABLE STATEMENT` (#1921) ([313a4](https://github.com/JSQLParser/JSqlParser/commit/313a4b42444b2d2) jxnu-liguobin) -- Support `RENAME INDEX` for MySQL, `RENAME CONSTRAINT` for PostgreSQL (#1920) ([989a8](https://github.com/JSQLParser/JSqlParser/commit/989a84bb215283b) jxnu-liguobin) -- Add support comment in `create view` for MySQL and MariaDb (#1913) ([4d47e](https://github.com/JSQLParser/JSqlParser/commit/4d47e0ab7bc2872) jxnu-liguobin) -- Add support for `REFRESH MATERIALIZED VIEW` (#1911) ([425c7](https://github.com/JSQLParser/JSqlParser/commit/425c72eb7d7f931) jxnu-liguobin) -- `SimpleFunction` for faster parsing of simple, but deep nested functions ([085d7](https://github.com/JSQLParser/JSqlParser/commit/085d7504235e58c) Andreas Reichel) -- add support for snowflake merge statements (#1887) ([36b80](https://github.com/JSQLParser/JSqlParser/commit/36b806dede06260) David Goss) -- `ColDataType` supports `PUBLIC` schema and all non-restricted keywords for type ([1088d](https://github.com/JSQLParser/JSqlParser/commit/1088db7aea0b2f9) Andreas Reichel) -- T-SQL Join Hints ([5f09e](https://github.com/JSQLParser/JSqlParser/commit/5f09ec4914fbdd1) Andreas Reichel) -- old TSQL Joins `*=` and `=*` ([0b50d](https://github.com/JSQLParser/JSqlParser/commit/0b50da4cca555b6) Andreas Reichel) -- MS SQL Server `Merge` `Output` clause ([7bd42](https://github.com/JSQLParser/JSqlParser/commit/7bd42edaa0d9aed) Andreas Reichel) -- MS SQL Server `UPDATE ...` Index Hint ([f919e](https://github.com/JSQLParser/JSqlParser/commit/f919e00c30ff5df) Andreas Reichel) -- Postgres `Contains` and `ContainedBy` Operators ([28a4c](https://github.com/JSQLParser/JSqlParser/commit/28a4c080b718aba) Andreas Reichel) -- Postgres `Contains` and `ContainedBy` Operators ([09d6d](https://github.com/JSQLParser/JSqlParser/commit/09d6dfe7bc7acb8) Andreas Reichel) -- Clickhouse `GLOBAL IN ...` ([ced0d](https://github.com/JSQLParser/JSqlParser/commit/ced0d0090c5c9a9) Andreas Reichel) -- `CREATE INDEX IF NOT EXISTS...` ([da13d](https://github.com/JSQLParser/JSqlParser/commit/da13d7dc1dd1608) Andreas Reichel) -- support clickhouse global keyword in IN Expression ([a9ed7](https://github.com/JSQLParser/JSqlParser/commit/a9ed79825110df7) hezw) +- support mysql with rollup (#1923) ([77f6f](https://github.com/JSQLParser/JSqlParser/commit/77f6fb8c92b3378) jxnu-liguobin) +- Support `FOR SHARE` (#1922) ([815f8](https://github.com/JSQLParser/JSqlParser/commit/815f8753d552d89) jxnu-liguobin) +- [MySQL] Support `TABLE STATEMENT` (#1921) ([313a4](https://github.com/JSQLParser/JSqlParser/commit/313a4b42444b2d2) jxnu-liguobin) +- Support `RENAME INDEX` for MySQL, `RENAME CONSTRAINT` for PostgreSQL (#1920) ([989a8](https://github.com/JSQLParser/JSqlParser/commit/989a84bb215283b) jxnu-liguobin) +- Add support comment in `create view` for MySQL and MariaDb (#1913) ([4d47e](https://github.com/JSQLParser/JSqlParser/commit/4d47e0ab7bc2872) jxnu-liguobin) +- Add support for `REFRESH MATERIALIZED VIEW` (#1911) ([425c7](https://github.com/JSQLParser/JSqlParser/commit/425c72eb7d7f931) jxnu-liguobin) +- `SimpleFunction` for faster parsing of simple, but deep nested functions ([085d7](https://github.com/JSQLParser/JSqlParser/commit/085d7504235e58c) Andreas Reichel) +- add support for snowflake merge statements (#1887) ([36b80](https://github.com/JSQLParser/JSqlParser/commit/36b806dede06260) David Goss) +- `ColDataType` supports `PUBLIC` schema and all non-restricted keywords for type ([1088d](https://github.com/JSQLParser/JSqlParser/commit/1088db7aea0b2f9) Andreas Reichel) +- T-SQL Join Hints ([5f09e](https://github.com/JSQLParser/JSqlParser/commit/5f09ec4914fbdd1) Andreas Reichel) +- old TSQL Joins `*=` and `=*` ([0b50d](https://github.com/JSQLParser/JSqlParser/commit/0b50da4cca555b6) Andreas Reichel) +- MS SQL Server `Merge` `Output` clause ([7bd42](https://github.com/JSQLParser/JSqlParser/commit/7bd42edaa0d9aed) Andreas Reichel) +- MS SQL Server `UPDATE ...` Index Hint ([f919e](https://github.com/JSQLParser/JSqlParser/commit/f919e00c30ff5df) Andreas Reichel) +- Postgres `Contains` and `ContainedBy` Operators ([28a4c](https://github.com/JSQLParser/JSqlParser/commit/28a4c080b718aba) Andreas Reichel) +- Postgres `Contains` and `ContainedBy` Operators ([09d6d](https://github.com/JSQLParser/JSqlParser/commit/09d6dfe7bc7acb8) Andreas Reichel) +- Clickhouse `GLOBAL IN ...` ([ced0d](https://github.com/JSQLParser/JSqlParser/commit/ced0d0090c5c9a9) Andreas Reichel) +- `CREATE INDEX IF NOT EXISTS...` ([da13d](https://github.com/JSQLParser/JSqlParser/commit/da13d7dc1dd1608) Andreas Reichel) +- support clickhouse global keyword in IN Expression ([a9ed7](https://github.com/JSQLParser/JSqlParser/commit/a9ed79825110df7) hezw) ### Bug Fixes -- refactor `JsonExpression`, avoiding expensive semantic lookahead and improving performance ([56515](https://github.com/JSQLParser/JSqlParser/commit/56515aba6ca893f) Andreas Reichel) -- `GO` shall terminate statement only, when appearing alone on an empty line ([14637](https://github.com/JSQLParser/JSqlParser/commit/14637ce64763b42) Andreas Reichel) -- De-Parse Oracle Hints in UPDATE, INSERT, DELETE and MERGE ([aaca0](https://github.com/JSQLParser/JSqlParser/commit/aaca05855f9a11b) Andreas Reichel) -- `UpdateSet` shall not have brackets with single element only ([15b9a](https://github.com/JSQLParser/JSqlParser/commit/15b9aef7ca05416) Andreas Reichel) -- make `GLOBAL` a restricted keyword, not usable as an Alias ([dd6cf](https://github.com/JSQLParser/JSqlParser/commit/dd6cf23150f4804) Andreas Reichel) -- Postgres `NextVal()` function ([e3afa](https://github.com/JSQLParser/JSqlParser/commit/e3afa5fbdebc715) Andreas Reichel) -- optional `Expression` in `FETCH` clause ([daee3](https://github.com/JSQLParser/JSqlParser/commit/daee30f7ae88bea) Andreas Reichel) -- allow `RAW` as `CreateParameter` ([ecd40](https://github.com/JSQLParser/JSqlParser/commit/ecd40386585a519) Andreas Reichel) +- refactor `JsonExpression`, avoiding expensive semantic lookahead and improving performance ([56515](https://github.com/JSQLParser/JSqlParser/commit/56515aba6ca893f) Andreas Reichel) +- `GO` shall terminate statement only, when appearing alone on an empty line ([14637](https://github.com/JSQLParser/JSqlParser/commit/14637ce64763b42) Andreas Reichel) +- De-Parse Oracle Hints in UPDATE, INSERT, DELETE and MERGE ([aaca0](https://github.com/JSQLParser/JSqlParser/commit/aaca05855f9a11b) Andreas Reichel) +- `UpdateSet` shall not have brackets with single element only ([15b9a](https://github.com/JSQLParser/JSqlParser/commit/15b9aef7ca05416) Andreas Reichel) +- make `GLOBAL` a restricted keyword, not usable as an Alias ([dd6cf](https://github.com/JSQLParser/JSqlParser/commit/dd6cf23150f4804) Andreas Reichel) +- Postgres `NextVal()` function ([e3afa](https://github.com/JSQLParser/JSqlParser/commit/e3afa5fbdebc715) Andreas Reichel) +- optional `Expression` in `FETCH` clause ([daee3](https://github.com/JSQLParser/JSqlParser/commit/daee30f7ae88bea) Andreas Reichel) +- allow `RAW` as `CreateParameter` ([ecd40](https://github.com/JSQLParser/JSqlParser/commit/ecd40386585a519) Andreas Reichel) ### Other changes @@ -928,14 +1800,14 @@ Changelog of JSqlParser. **Fix typo in migration.rst (#1888)** -* Found a typo in the 4.7 migration document. Trivial PR. Please merge. +* Found a typo in the 4.7 migration document. Trivial PR. Please merge. [902e4](https://github.com/JSQLParser/JSqlParser/commit/902e4c46f783985) Ed Sabol *2023-11-10 03:05:39* **Unit tests support multi-os and higher versions of jdk (#1886)** -* fix: tokenBlockPattern support \r\n or \r -* test: remove nashorn ignore annotation to support jdk11+ +* fix: tokenBlockPattern support \r\n or \r +* test: remove nashorn ignore annotation to support jdk11+ [97e92](https://github.com/JSQLParser/JSqlParser/commit/97e9229d15df7d6) human-user *2023-11-08 03:07:04* @@ -969,65 +1841,65 @@ Changelog of JSqlParser. ### Breaking changes -- add support for INTERPRET function parsing (#1816) ([180ec](https://github.com/JSQLParser/JSqlParser/commit/180ec68cc9fa7eb) Matteo Sist) -- Remove `ItemsList`, `MultiExpressionList`, `Replace` ([14170](https://github.com/JSQLParser/JSqlParser/commit/141708eabc4f2ea) Andreas Reichel) -- Consolidate the `ExpressionList`, removing many redundant List alike Classes and Productions ([288b1](https://github.com/JSQLParser/JSqlParser/commit/288b177fe9c8a4c) Andreas Reichel) -- remove `SelectExpressionItem` in favor of `SelectItem` ([b9057](https://github.com/JSQLParser/JSqlParser/commit/b9057d2b75cd1d7) Andreas Reichel) -- ClickHouse `Select...` ``FINAL` modifier ([4b7f2](https://github.com/JSQLParser/JSqlParser/commit/4b7f21c54c24d04) Andreas Reichel) +- add support for INTERPRET function parsing (#1816) ([180ec](https://github.com/JSQLParser/JSqlParser/commit/180ec68cc9fa7eb) Matteo Sist) +- Remove `ItemsList`, `MultiExpressionList`, `Replace` ([14170](https://github.com/JSQLParser/JSqlParser/commit/141708eabc4f2ea) Andreas Reichel) +- Consolidate the `ExpressionList`, removing many redundant List alike Classes and Productions ([288b1](https://github.com/JSQLParser/JSqlParser/commit/288b177fe9c8a4c) Andreas Reichel) +- remove `SelectExpressionItem` in favor of `SelectItem` ([b9057](https://github.com/JSQLParser/JSqlParser/commit/b9057d2b75cd1d7) Andreas Reichel) +- ClickHouse `Select...` ``FINAL` modifier ([4b7f2](https://github.com/JSQLParser/JSqlParser/commit/4b7f21c54c24d04) Andreas Reichel) ### Features -- H2 BYTEA Values `X'01' '02'` ([54828](https://github.com/JSQLParser/JSqlParser/commit/54828a456a7f192) Andreas Reichel) -- BigQuery Except(..) Replace(..) syntax ([4b4ae](https://github.com/JSQLParser/JSqlParser/commit/4b4ae04f44ff18b) Andreas Reichel) -- implement a few missing expressions ([04128](https://github.com/JSQLParser/JSqlParser/commit/0412897f9ea809f) Andreas Reichel) -- SQL:2016 TABLESAMPLE clause ([4d8a5](https://github.com/JSQLParser/JSqlParser/commit/4d8a512191a4a1b) Andreas Reichel) -- add a method checking balanced brackets ([52df3](https://github.com/JSQLParser/JSqlParser/commit/52df32dd8ec2c10) Andreas Reichel) -- add support for INTERPRET function parsing (#1816) ([180ec](https://github.com/JSQLParser/JSqlParser/commit/180ec68cc9fa7eb) Matteo Sist) -- MySQL `NOT RLIKE`, `NOT REGEXP` expressions ([f1325](https://github.com/JSQLParser/JSqlParser/commit/f132547f56a1edd) Andreas Reichel) -- Postgres `NOTNULL` support ([386dc](https://github.com/JSQLParser/JSqlParser/commit/386dc7a0df98f1c) manticore-projects) -- `QUALIFY` clause ([75e4d](https://github.com/JSQLParser/JSqlParser/commit/75e4d30747a7e6e) Andreas Reichel) -- T-SQL `FOR ...` clause ([8027d](https://github.com/JSQLParser/JSqlParser/commit/8027dbf2cbf9163) Andreas Reichel) -- Quoted Identifiers can contain double-quotes (PostgreSQL) ([73c55](https://github.com/JSQLParser/JSqlParser/commit/73c55fda1ac6a42) Andreas Reichel) -- functions blocks, parenthesed JSON Expressions ([5263b](https://github.com/JSQLParser/JSqlParser/commit/5263b91f3e555b7) Andreas Reichel) -- functions blocks, parenthesed JSON Expressions ([e19dc](https://github.com/JSQLParser/JSqlParser/commit/e19dc0e081f741d) Andreas Reichel) -- parse CREATE TRIGGER as UnsupportedStatement ([64b03](https://github.com/JSQLParser/JSqlParser/commit/64b0331f772278b) Andreas Reichel) -- chaining JSON Expressions ([6ef5e](https://github.com/JSQLParser/JSqlParser/commit/6ef5e0b6ee06211) Andreas Reichel) -- Write API documentation to the WebSite via XMLDoclet ([c5366](https://github.com/JSQLParser/JSqlParser/commit/c53667f8eff30e3) Andreas Reichel) -- `MEMBER OF` condition as shown at https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#operator_member-of ([6e7a7](https://github.com/JSQLParser/JSqlParser/commit/6e7a78dfc563749) Andreas Reichel) -- access Elements of Array Columns ([09a70](https://github.com/JSQLParser/JSqlParser/commit/09a70a499121792) Andreas Reichel) -- JdbcNamedParameter allows "&" (instead of ":") ([c07a4](https://github.com/JSQLParser/JSqlParser/commit/c07a43b3c128a5d) Andreas Reichel) -- Consolidate the `ExpressionList`, removing many redundant List alike Classes and Productions ([288b1](https://github.com/JSQLParser/JSqlParser/commit/288b177fe9c8a4c) Andreas Reichel) -- ClickHouse `LIMIT ... BY ...` clause ([4d5e2](https://github.com/JSQLParser/JSqlParser/commit/4d5e26d3febe686) Andreas Reichel) -- implement SQL:2016 Convert() and Trim() ([3a27a](https://github.com/JSQLParser/JSqlParser/commit/3a27a9dd4add700) Andreas Reichel) -- Switch off contradicting `JOIN` qualifiers, when setting a qualifier ([b6ea8](https://github.com/JSQLParser/JSqlParser/commit/b6ea8b162450545) Andreas Reichel) -- Test if a JOIN is an INNER JOIN according to the SQL:2016 ([6281b](https://github.com/JSQLParser/JSqlParser/commit/6281b07a543b088) Andreas Reichel) -- ClickHouse `Select...` ``FINAL` modifier ([4b7f2](https://github.com/JSQLParser/JSqlParser/commit/4b7f21c54c24d04) Andreas Reichel) -- Multi-Part Names for Variables and Parameters ([9da7a](https://github.com/JSQLParser/JSqlParser/commit/9da7a06ebe9b036) Andreas Reichel) -- Oracle `HAVING` before `GROUP BY` ([4efb9](https://github.com/JSQLParser/JSqlParser/commit/4efb99f1510ad16) Andreas Reichel) -- Lateral View ([8a1bd](https://github.com/JSQLParser/JSqlParser/commit/8a1bdeccbadb04f) Andreas Reichel) -- FETCH uses EXPRESSION ([0979b](https://github.com/JSQLParser/JSqlParser/commit/0979b2e5ea76b8c) Andreas Reichel) -- Support more Statement Separators ([b0814](https://github.com/JSQLParser/JSqlParser/commit/b08148414bd8f30) Andreas Reichel) -- CREATE VIEW ... REFRESH AUTO... ([1c8d8](https://github.com/JSQLParser/JSqlParser/commit/1c8d8daf48ebac1) Andreas Reichel) -- Oracle Alternative Quoting ([c57c4](https://github.com/JSQLParser/JSqlParser/commit/c57c427032c91d0) Andreas Reichel) -- make important Classes Serializable ([b94b2](https://github.com/JSQLParser/JSqlParser/commit/b94b2cc6a8f8c7d) Andreas Reichel) +- H2 BYTEA Values `X'01' '02'` ([54828](https://github.com/JSQLParser/JSqlParser/commit/54828a456a7f192) Andreas Reichel) +- BigQuery Except(..) Replace(..) syntax ([4b4ae](https://github.com/JSQLParser/JSqlParser/commit/4b4ae04f44ff18b) Andreas Reichel) +- implement a few missing expressions ([04128](https://github.com/JSQLParser/JSqlParser/commit/0412897f9ea809f) Andreas Reichel) +- SQL:2016 TABLESAMPLE clause ([4d8a5](https://github.com/JSQLParser/JSqlParser/commit/4d8a512191a4a1b) Andreas Reichel) +- add a method checking balanced brackets ([52df3](https://github.com/JSQLParser/JSqlParser/commit/52df32dd8ec2c10) Andreas Reichel) +- add support for INTERPRET function parsing (#1816) ([180ec](https://github.com/JSQLParser/JSqlParser/commit/180ec68cc9fa7eb) Matteo Sist) +- MySQL `NOT RLIKE`, `NOT REGEXP` expressions ([f1325](https://github.com/JSQLParser/JSqlParser/commit/f132547f56a1edd) Andreas Reichel) +- Postgres `NOTNULL` support ([386dc](https://github.com/JSQLParser/JSqlParser/commit/386dc7a0df98f1c) manticore-projects) +- `QUALIFY` clause ([75e4d](https://github.com/JSQLParser/JSqlParser/commit/75e4d30747a7e6e) Andreas Reichel) +- T-SQL `FOR ...` clause ([8027d](https://github.com/JSQLParser/JSqlParser/commit/8027dbf2cbf9163) Andreas Reichel) +- Quoted Identifiers can contain double-quotes (PostgreSQL) ([73c55](https://github.com/JSQLParser/JSqlParser/commit/73c55fda1ac6a42) Andreas Reichel) +- functions blocks, parenthesed JSON Expressions ([5263b](https://github.com/JSQLParser/JSqlParser/commit/5263b91f3e555b7) Andreas Reichel) +- functions blocks, parenthesed JSON Expressions ([e19dc](https://github.com/JSQLParser/JSqlParser/commit/e19dc0e081f741d) Andreas Reichel) +- parse CREATE TRIGGER as UnsupportedStatement ([64b03](https://github.com/JSQLParser/JSqlParser/commit/64b0331f772278b) Andreas Reichel) +- chaining JSON Expressions ([6ef5e](https://github.com/JSQLParser/JSqlParser/commit/6ef5e0b6ee06211) Andreas Reichel) +- Write API documentation to the WebSite via XMLDoclet ([c5366](https://github.com/JSQLParser/JSqlParser/commit/c53667f8eff30e3) Andreas Reichel) +- `MEMBER OF` condition as shown at https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#operator_member-of ([6e7a7](https://github.com/JSQLParser/JSqlParser/commit/6e7a78dfc563749) Andreas Reichel) +- access Elements of Array Columns ([09a70](https://github.com/JSQLParser/JSqlParser/commit/09a70a499121792) Andreas Reichel) +- JdbcNamedParameter allows "&" (instead of ":") ([c07a4](https://github.com/JSQLParser/JSqlParser/commit/c07a43b3c128a5d) Andreas Reichel) +- Consolidate the `ExpressionList`, removing many redundant List alike Classes and Productions ([288b1](https://github.com/JSQLParser/JSqlParser/commit/288b177fe9c8a4c) Andreas Reichel) +- ClickHouse `LIMIT ... BY ...` clause ([4d5e2](https://github.com/JSQLParser/JSqlParser/commit/4d5e26d3febe686) Andreas Reichel) +- implement SQL:2016 Convert() and Trim() ([3a27a](https://github.com/JSQLParser/JSqlParser/commit/3a27a9dd4add700) Andreas Reichel) +- Switch off contradicting `JOIN` qualifiers, when setting a qualifier ([b6ea8](https://github.com/JSQLParser/JSqlParser/commit/b6ea8b162450545) Andreas Reichel) +- Test if a JOIN is an INNER JOIN according to the SQL:2016 ([6281b](https://github.com/JSQLParser/JSqlParser/commit/6281b07a543b088) Andreas Reichel) +- ClickHouse `Select...` ``FINAL` modifier ([4b7f2](https://github.com/JSQLParser/JSqlParser/commit/4b7f21c54c24d04) Andreas Reichel) +- Multi-Part Names for Variables and Parameters ([9da7a](https://github.com/JSQLParser/JSqlParser/commit/9da7a06ebe9b036) Andreas Reichel) +- Oracle `HAVING` before `GROUP BY` ([4efb9](https://github.com/JSQLParser/JSqlParser/commit/4efb99f1510ad16) Andreas Reichel) +- Lateral View ([8a1bd](https://github.com/JSQLParser/JSqlParser/commit/8a1bdeccbadb04f) Andreas Reichel) +- FETCH uses EXPRESSION ([0979b](https://github.com/JSQLParser/JSqlParser/commit/0979b2e5ea76b8c) Andreas Reichel) +- Support more Statement Separators ([b0814](https://github.com/JSQLParser/JSqlParser/commit/b08148414bd8f30) Andreas Reichel) +- CREATE VIEW ... REFRESH AUTO... ([1c8d8](https://github.com/JSQLParser/JSqlParser/commit/1c8d8daf48ebac1) Andreas Reichel) +- Oracle Alternative Quoting ([c57c4](https://github.com/JSQLParser/JSqlParser/commit/c57c427032c91d0) Andreas Reichel) +- make important Classes Serializable ([b94b2](https://github.com/JSQLParser/JSqlParser/commit/b94b2cc6a8f8c7d) Andreas Reichel) ### Bug Fixes -- ExpressionList of Expressions in `Values` ([994e6](https://github.com/JSQLParser/JSqlParser/commit/994e6c63d065a48) Andreas Reichel) -- check for NULL before iterating ([beb68](https://github.com/JSQLParser/JSqlParser/commit/beb68d55239da97) Andreas Reichel) -- Backslash escaped single quote `'\''` ([a2975](https://github.com/JSQLParser/JSqlParser/commit/a29754341adeffc) Andreas Reichel) -- `INSERT` must use simple Column Names only ([420d7](https://github.com/JSQLParser/JSqlParser/commit/420d7d834760f14) Andreas Reichel) -- SPHINX modules and themes ([6f277](https://github.com/JSQLParser/JSqlParser/commit/6f277654b9344ec) Andreas Reichel) -- expose IntervalExpression attributes and use DeParser ([b6fab](https://github.com/JSQLParser/JSqlParser/commit/b6fab2a484e0b47) Andreas Reichel) -- throw the specific exception ([cb960](https://github.com/JSQLParser/JSqlParser/commit/cb960a35647a19a) Andreas Reichel) -- Complex Parsing Approach ([4f048](https://github.com/JSQLParser/JSqlParser/commit/4f0488ccb4611f0) Andreas Reichel) -- issue #1789 ([32ec5](https://github.com/JSQLParser/JSqlParser/commit/32ec56114c1fbc4) Andreas Reichel) -- issue #1789 ([d20c8](https://github.com/JSQLParser/JSqlParser/commit/d20c8e94de64e2a) Andreas Reichel) -- issue #1791 ([88d1b](https://github.com/JSQLParser/JSqlParser/commit/88d1b62f0038a9a) Andreas Reichel) -- Java Version 8 ([7cecd](https://github.com/JSQLParser/JSqlParser/commit/7cecd293cf4e0ea) Andreas Reichel) -- find the correct position when field belongs to an internal class ([21389](https://github.com/JSQLParser/JSqlParser/commit/21389b712995674) Andreas Reichel) -- Remove tests for `()`, since `ParenthesedExpressionList` will catch those too ([905ef](https://github.com/JSQLParser/JSqlParser/commit/905ef6512d592d6) Andreas Reichel) -- assign Enum case insensitive ([fc577](https://github.com/JSQLParser/JSqlParser/commit/fc577caa4146878) Andreas Reichel) +- ExpressionList of Expressions in `Values` ([994e6](https://github.com/JSQLParser/JSqlParser/commit/994e6c63d065a48) Andreas Reichel) +- check for NULL before iterating ([beb68](https://github.com/JSQLParser/JSqlParser/commit/beb68d55239da97) Andreas Reichel) +- Backslash escaped single quote `'\''` ([a2975](https://github.com/JSQLParser/JSqlParser/commit/a29754341adeffc) Andreas Reichel) +- `INSERT` must use simple Column Names only ([420d7](https://github.com/JSQLParser/JSqlParser/commit/420d7d834760f14) Andreas Reichel) +- SPHINX modules and themes ([6f277](https://github.com/JSQLParser/JSqlParser/commit/6f277654b9344ec) Andreas Reichel) +- expose IntervalExpression attributes and use DeParser ([b6fab](https://github.com/JSQLParser/JSqlParser/commit/b6fab2a484e0b47) Andreas Reichel) +- throw the specific exception ([cb960](https://github.com/JSQLParser/JSqlParser/commit/cb960a35647a19a) Andreas Reichel) +- Complex Parsing Approach ([4f048](https://github.com/JSQLParser/JSqlParser/commit/4f0488ccb4611f0) Andreas Reichel) +- issue #1789 ([32ec5](https://github.com/JSQLParser/JSqlParser/commit/32ec56114c1fbc4) Andreas Reichel) +- issue #1789 ([d20c8](https://github.com/JSQLParser/JSqlParser/commit/d20c8e94de64e2a) Andreas Reichel) +- issue #1791 ([88d1b](https://github.com/JSQLParser/JSqlParser/commit/88d1b62f0038a9a) Andreas Reichel) +- Java Version 8 ([7cecd](https://github.com/JSQLParser/JSqlParser/commit/7cecd293cf4e0ea) Andreas Reichel) +- find the correct position when field belongs to an internal class ([21389](https://github.com/JSQLParser/JSqlParser/commit/21389b712995674) Andreas Reichel) +- Remove tests for `()`, since `ParenthesedExpressionList` will catch those too ([905ef](https://github.com/JSQLParser/JSqlParser/commit/905ef6512d592d6) Andreas Reichel) +- assign Enum case insensitive ([fc577](https://github.com/JSQLParser/JSqlParser/commit/fc577caa4146878) Andreas Reichel) ### Other changes @@ -1038,9 +1910,9 @@ Changelog of JSqlParser. **Fixing a problem with an OP_CONCAT in WhenExpression (#1837)** -* fix: Concatenation in inner ELSE statement (Second level of Case Expression) -* fix: broken tests -* fix: Delete lookahead(3) +* fix: Concatenation in inner ELSE statement (Second level of Case Expression) +* fix: broken tests +* fix: Delete lookahead(3) [f05cb](https://github.com/JSQLParser/JSqlParser/commit/f05cb7ff4aa46c5) amigalev *2023-08-20 04:43:30* @@ -1066,7 +1938,7 @@ Changelog of JSqlParser. **Update sphinx.yml** -* fix the FURO theme +* fix the FURO theme [51cc4](https://github.com/JSQLParser/JSqlParser/commit/51cc444ff98ad1d) manticore-projects *2023-06-01 02:49:23* @@ -1087,157 +1959,157 @@ Changelog of JSqlParser. **Fix #1758: Use long for Feature.timeOut (#1759)** -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Co-authored-by: Tobias <t.warneke@gmx.net> [3314e](https://github.com/JSQLParser/JSqlParser/commit/3314edf0ea17772) Tomasz Zarna *2023-04-27 20:30:31* **Ignoring unnecessarily generated jacoco report (#1762)** -* Ignoring unnecessarily generated jacoco report -* Ignoring unnecessarily generated by pmd plugin -* --------- -* Co-authored-by: other <other@ECE-A55006.austin.utexas.edu> -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Ignoring unnecessarily generated jacoco report +* Ignoring unnecessarily generated by pmd plugin +* --------- +* Co-authored-by: other <other@ECE-A55006.austin.utexas.edu> +* Co-authored-by: Tobias <t.warneke@gmx.net> [1bbb1](https://github.com/JSQLParser/JSqlParser/commit/1bbb1443d84684c) optimizing-ci-builds *2023-04-27 19:50:42* **Ignoring unnecessarily generated by pmd plugin (#1763)** -* Co-authored-by: other <other@ECE-A55006.austin.utexas.edu> +* Co-authored-by: other <other@ECE-A55006.austin.utexas.edu> [52648](https://github.com/JSQLParser/JSqlParser/commit/52648277e69fa07) optimizing-ci-builds *2023-04-27 19:49:15* **Refactor Parenthesed SelectBody and FromItem (#1754)** -* Fixes #1684: Support CREATE MATERIALIZED VIEW with AUTO REFRESH -* Support parsing create view statements in Redshift with AUTO REFRESH -* option. -* Reduce cyclomatic complexity in CreateView.toString -* Extract adding the force option into a dedicated method resulting in the -* cyclomatic complexity reduction of the CreateView.toString method. -* Enhanced Keywords -* Add Keywords and document, which keywords are allowed for what purpose -* Fix incorrect tests -* Define Reserved Keywords explicitly -* Derive All Keywords from Grammar directly -* Generate production for Object Names (semi-) automatically -* Add parametrized Keyword Tests -* Fix test resources -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Sphinx Documentation -* Update the MANTICORE Sphinx Theme, but ignore it in GIT -* Add the content to the Sphinx sites -* Add a Gradle function to derive Stable and Snapshot version from GIT Tags -* Add a Gradle GIT change task -* Add a Gradle sphinx task -* Add a special Test case for illustrating the use of JSQLParser -* doc: request for `Conventional Commit` messages -* feat: make important Classes Serializable -* Implement Serializable for persisting via ObjectOutputStream -* chore: Make Serializable -* doc: Better integration of the RR diagrams -* - apply neutral Sphinx theme -* - insert the RR diagrams into the sphinx sources -* - better documentation on Gradle dependencies -* - link GitHub repository -* Merge -* feat: Oracle Alternative Quoting -* - add support for Oracle Alternative Quoting e.g. `q'(...)'` -* - fixes #1718 -* - add a Logo and FavIcon to the Website -* - document recent changes on Quoting/Escaping -* - add an example on building SQL from Java -* - rework the README.md, promote the Website -* - add Spotless Formatter, using Google Java Style (with Tab=4 Spaces) -* style: Appease PMD/Codacy -* doc: fix the issue template -* - fix the issue template -* - fix the -SNAPSHOT version number -* Update issue templates -* Update issue templates -* feat: Support more Statement Separators -* - `GO` -* - Slash `/` -* - Two empty lines -* feat: FETCH uses EXPRESSION -* - `FETCH` uses `EXPRESSION` instead of SimpleJDBCParameter only -* - Visit/Accept `FETCH` `EXPRESSION` instead of `append` to String -* - Visit/Accept `OFFSET` `EXPRESSION` instead of `append` to String -* - Gradle: remove obsolete/incompatible `jvmArgs` from Test() -* style: apply Spotless -* test: commit missing test -* fix: JSon Operator can use Simple Function -* Supports `Function() ->> Literal` (although `Function()` would not allow Nested Expression Parameters) -* fixes #1571 -* style: Reformat changed files and headers -* style: Remove unused variable -* feat: Add support for Hangul "\uAC00"-"\uD7A3" -* fixes #1747 -* style: expose `SetStatement` key-value list -* fixes #1746 -* style: Appease PMD/Codacy -* feat: `ConflictTarget` allows multiple `IndexColumnNames` -* fixes #1749 -* fixes #1633 -* fixes #955 -* doc: fix reference in the Java Doc -* build: better Upload Groovy Task -* feat: ParenthesedSelectBody and ParenthesedFromItem -* - First properly working version -* - Work in progress, 13 tests failing -* feat: ParenthesedSelectBody and ParenthesedFromItem -* - delete unneeded ParenthesedJoin -* - rename ParenthesisFromItem into ParenthesedFromItem -* feat: ParenthesedSelectBody and ParenthesedFromItem -* - fix `NULLS FIRST` and `NULLS LAST` -* feat: ParenthesedSelectBody and ParenthesedFromItem -* - fix Oracle Hints -* feat: ParenthesedSelectBody and ParenthesedFromItem -* - parse `SetOperation` only after a (first plain) SelectBody has found, this fixes the performance issue -* - one more special Oracle Test succeeds -* - 5 remaining test failures -* feat: ParenthesedSelectBody and ParenthesedFromItem -* - extract `OrderByElements` into `SelectBody` -* - one more special Oracle Test succeeds -* - all tests succeed -* style: Appease PMD/Codacy -* style: Appease PMD/Codacy -* feat: Refactor SelectBody implementations -* - `SelectBody` implements `FromItem` -* - get rid of `SubSelect` and `SpecialSubSelect` -* - `Merge` can use `FromItem` instead of `SubSelect` or `Table` -* - `LateralSubSelect` extends `ParenthesedSelectBody` directly -* - Simplify the `Select` statement, although it is still redundant since `SelectBody` also could implement `Statement` directly -* - `WithItem` can use `SelectBody` directly, which allows for nested `WithItems` -* BREAKING-CHANGE: Lots of redundant methods and intermediate removed -* feat: Refactor SelectBody implementations -* - `SelectBody` implements `Statement` and so makes `Select` redundant -* - get rid of `ValuesList` -* - refactor `ValuesStatement` into `Values` which just implements `SelectBody` (and becomes a `Statement` and a `FromItem`), move to `select` package -* BREAKING-CHANGE: Lots of redundant methods and intermediate removed -* style: Code cleanup -* - remove 3 unused/obsolete productions -* - appease PMD/Codacy -* feat: Merge `SelectBody` into `Select` Statement -* - former `SelectBody` implements `Statement` and so becomes `Select` -* - this reduces the AST by 1 hierarchy level -* style: Remove unused import -* test: @Disabled invalid Test -* style: Appease PMD/Codacy -* test: Add a SubSelect Parsing Test -* --------- -* Co-authored-by: zaza <tzarna@gmail.com> +* Fixes #1684: Support CREATE MATERIALIZED VIEW with AUTO REFRESH +* Support parsing create view statements in Redshift with AUTO REFRESH +* option. +* Reduce cyclomatic complexity in CreateView.toString +* Extract adding the force option into a dedicated method resulting in the +* cyclomatic complexity reduction of the CreateView.toString method. +* Enhanced Keywords +* Add Keywords and document, which keywords are allowed for what purpose +* Fix incorrect tests +* Define Reserved Keywords explicitly +* Derive All Keywords from Grammar directly +* Generate production for Object Names (semi-) automatically +* Add parametrized Keyword Tests +* Fix test resources +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Sphinx Documentation +* Update the MANTICORE Sphinx Theme, but ignore it in GIT +* Add the content to the Sphinx sites +* Add a Gradle function to derive Stable and Snapshot version from GIT Tags +* Add a Gradle GIT change task +* Add a Gradle sphinx task +* Add a special Test case for illustrating the use of JSQLParser +* doc: request for `Conventional Commit` messages +* feat: make important Classes Serializable +* Implement Serializable for persisting via ObjectOutputStream +* chore: Make Serializable +* doc: Better integration of the RR diagrams +* - apply neutral Sphinx theme +* - insert the RR diagrams into the sphinx sources +* - better documentation on Gradle dependencies +* - link GitHub repository +* Merge +* feat: Oracle Alternative Quoting +* - add support for Oracle Alternative Quoting e.g. `q'(...)'` +* - fixes #1718 +* - add a Logo and FavIcon to the Website +* - document recent changes on Quoting/Escaping +* - add an example on building SQL from Java +* - rework the README.md, promote the Website +* - add Spotless Formatter, using Google Java Style (with Tab=4 Spaces) +* style: Appease PMD/Codacy +* doc: fix the issue template +* - fix the issue template +* - fix the -SNAPSHOT version number +* Update issue templates +* Update issue templates +* feat: Support more Statement Separators +* - `GO` +* - Slash `/` +* - Two empty lines +* feat: FETCH uses EXPRESSION +* - `FETCH` uses `EXPRESSION` instead of SimpleJDBCParameter only +* - Visit/Accept `FETCH` `EXPRESSION` instead of `append` to String +* - Visit/Accept `OFFSET` `EXPRESSION` instead of `append` to String +* - Gradle: remove obsolete/incompatible `jvmArgs` from Test() +* style: apply Spotless +* test: commit missing test +* fix: JSon Operator can use Simple Function +* Supports `Function() ->> Literal` (although `Function()` would not allow Nested Expression Parameters) +* fixes #1571 +* style: Reformat changed files and headers +* style: Remove unused variable +* feat: Add support for Hangul "\uAC00"-"\uD7A3" +* fixes #1747 +* style: expose `SetStatement` key-value list +* fixes #1746 +* style: Appease PMD/Codacy +* feat: `ConflictTarget` allows multiple `IndexColumnNames` +* fixes #1749 +* fixes #1633 +* fixes #955 +* doc: fix reference in the Java Doc +* build: better Upload Groovy Task +* feat: ParenthesedSelectBody and ParenthesedFromItem +* - First properly working version +* - Work in progress, 13 tests failing +* feat: ParenthesedSelectBody and ParenthesedFromItem +* - delete unneeded ParenthesedJoin +* - rename ParenthesisFromItem into ParenthesedFromItem +* feat: ParenthesedSelectBody and ParenthesedFromItem +* - fix `NULLS FIRST` and `NULLS LAST` +* feat: ParenthesedSelectBody and ParenthesedFromItem +* - fix Oracle Hints +* feat: ParenthesedSelectBody and ParenthesedFromItem +* - parse `SetOperation` only after a (first plain) SelectBody has found, this fixes the performance issue +* - one more special Oracle Test succeeds +* - 5 remaining test failures +* feat: ParenthesedSelectBody and ParenthesedFromItem +* - extract `OrderByElements` into `SelectBody` +* - one more special Oracle Test succeeds +* - all tests succeed +* style: Appease PMD/Codacy +* style: Appease PMD/Codacy +* feat: Refactor SelectBody implementations +* - `SelectBody` implements `FromItem` +* - get rid of `SubSelect` and `SpecialSubSelect` +* - `Merge` can use `FromItem` instead of `SubSelect` or `Table` +* - `LateralSubSelect` extends `ParenthesedSelectBody` directly +* - Simplify the `Select` statement, although it is still redundant since `SelectBody` also could implement `Statement` directly +* - `WithItem` can use `SelectBody` directly, which allows for nested `WithItems` +* BREAKING-CHANGE: Lots of redundant methods and intermediate removed +* feat: Refactor SelectBody implementations +* - `SelectBody` implements `Statement` and so makes `Select` redundant +* - get rid of `ValuesList` +* - refactor `ValuesStatement` into `Values` which just implements `SelectBody` (and becomes a `Statement` and a `FromItem`), move to `select` package +* BREAKING-CHANGE: Lots of redundant methods and intermediate removed +* style: Code cleanup +* - remove 3 unused/obsolete productions +* - appease PMD/Codacy +* feat: Merge `SelectBody` into `Select` Statement +* - former `SelectBody` implements `Statement` and so becomes `Select` +* - this reduces the AST by 1 hierarchy level +* style: Remove unused import +* test: @Disabled invalid Test +* style: Appease PMD/Codacy +* test: Add a SubSelect Parsing Test +* --------- +* Co-authored-by: zaza <tzarna@gmail.com> [a312d](https://github.com/JSQLParser/JSqlParser/commit/a312dcdc2d618f1) manticore-projects *2023-04-27 19:38:24* @@ -1248,181 +2120,181 @@ Changelog of JSqlParser. **Assorted Fixes #7 (#1745)** -* Fixes #1684: Support CREATE MATERIALIZED VIEW with AUTO REFRESH -* Support parsing create view statements in Redshift with AUTO REFRESH -* option. -* Reduce cyclomatic complexity in CreateView.toString -* Extract adding the force option into a dedicated method resulting in the -* cyclomatic complexity reduction of the CreateView.toString method. -* Enhanced Keywords -* Add Keywords and document, which keywords are allowed for what purpose -* Fix incorrect tests -* Define Reserved Keywords explicitly -* Derive All Keywords from Grammar directly -* Generate production for Object Names (semi-) automatically -* Add parametrized Keyword Tests -* Fix test resources -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Sphinx Documentation -* Update the MANTICORE Sphinx Theme, but ignore it in GIT -* Add the content to the Sphinx sites -* Add a Gradle function to derive Stable and Snapshot version from GIT Tags -* Add a Gradle GIT change task -* Add a Gradle sphinx task -* Add a special Test case for illustrating the use of JSQLParser -* doc: request for `Conventional Commit` messages -* feat: make important Classes Serializable -* Implement Serializable for persisting via ObjectOutputStream -* chore: Make Serializable -* doc: Better integration of the RR diagrams -* - apply neutral Sphinx theme -* - insert the RR diagrams into the sphinx sources -* - better documentation on Gradle dependencies -* - link GitHub repository -* Merge -* feat: Oracle Alternative Quoting -* - add support for Oracle Alternative Quoting e.g. `q'(...)'` -* - fixes #1718 -* - add a Logo and FavIcon to the Website -* - document recent changes on Quoting/Escaping -* - add an example on building SQL from Java -* - rework the README.md, promote the Website -* - add Spotless Formatter, using Google Java Style (with Tab=4 Spaces) -* style: Appease PMD/Codacy -* doc: fix the issue template -* - fix the issue template -* - fix the -SNAPSHOT version number -* Update issue templates -* Update issue templates -* feat: Support more Statement Separators -* - `GO` -* - Slash `/` -* - Two empty lines -* feat: FETCH uses EXPRESSION -* - `FETCH` uses `EXPRESSION` instead of SimpleJDBCParameter only -* - Visit/Accept `FETCH` `EXPRESSION` instead of `append` to String -* - Visit/Accept `OFFSET` `EXPRESSION` instead of `append` to String -* - Gradle: remove obsolete/incompatible `jvmArgs` from Test() -* style: apply Spotless -* test: commit missing test -* fix: JSon Operator can use Simple Function -* Supports `Function() ->> Literal` (although `Function()` would not allow Nested Expression Parameters) -* fixes #1571 -* style: Reformat changed files and headers -* style: Remove unused variable -* feat: Add support for Hangul "\uAC00"-"\uD7A3" -* fixes #1747 -* style: expose `SetStatement` key-value list -* fixes #1746 -* style: Appease PMD/Codacy -* feat: `ConflictTarget` allows multiple `IndexColumnNames` -* fixes #1749 -* fixes #1633 -* fixes #955 -* doc: fix reference in the Java Doc -* build: better Upload Groovy Task -* --------- -* Co-authored-by: zaza <tzarna@gmail.com> +* Fixes #1684: Support CREATE MATERIALIZED VIEW with AUTO REFRESH +* Support parsing create view statements in Redshift with AUTO REFRESH +* option. +* Reduce cyclomatic complexity in CreateView.toString +* Extract adding the force option into a dedicated method resulting in the +* cyclomatic complexity reduction of the CreateView.toString method. +* Enhanced Keywords +* Add Keywords and document, which keywords are allowed for what purpose +* Fix incorrect tests +* Define Reserved Keywords explicitly +* Derive All Keywords from Grammar directly +* Generate production for Object Names (semi-) automatically +* Add parametrized Keyword Tests +* Fix test resources +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Sphinx Documentation +* Update the MANTICORE Sphinx Theme, but ignore it in GIT +* Add the content to the Sphinx sites +* Add a Gradle function to derive Stable and Snapshot version from GIT Tags +* Add a Gradle GIT change task +* Add a Gradle sphinx task +* Add a special Test case for illustrating the use of JSQLParser +* doc: request for `Conventional Commit` messages +* feat: make important Classes Serializable +* Implement Serializable for persisting via ObjectOutputStream +* chore: Make Serializable +* doc: Better integration of the RR diagrams +* - apply neutral Sphinx theme +* - insert the RR diagrams into the sphinx sources +* - better documentation on Gradle dependencies +* - link GitHub repository +* Merge +* feat: Oracle Alternative Quoting +* - add support for Oracle Alternative Quoting e.g. `q'(...)'` +* - fixes #1718 +* - add a Logo and FavIcon to the Website +* - document recent changes on Quoting/Escaping +* - add an example on building SQL from Java +* - rework the README.md, promote the Website +* - add Spotless Formatter, using Google Java Style (with Tab=4 Spaces) +* style: Appease PMD/Codacy +* doc: fix the issue template +* - fix the issue template +* - fix the -SNAPSHOT version number +* Update issue templates +* Update issue templates +* feat: Support more Statement Separators +* - `GO` +* - Slash `/` +* - Two empty lines +* feat: FETCH uses EXPRESSION +* - `FETCH` uses `EXPRESSION` instead of SimpleJDBCParameter only +* - Visit/Accept `FETCH` `EXPRESSION` instead of `append` to String +* - Visit/Accept `OFFSET` `EXPRESSION` instead of `append` to String +* - Gradle: remove obsolete/incompatible `jvmArgs` from Test() +* style: apply Spotless +* test: commit missing test +* fix: JSon Operator can use Simple Function +* Supports `Function() ->> Literal` (although `Function()` would not allow Nested Expression Parameters) +* fixes #1571 +* style: Reformat changed files and headers +* style: Remove unused variable +* feat: Add support for Hangul "\uAC00"-"\uD7A3" +* fixes #1747 +* style: expose `SetStatement` key-value list +* fixes #1746 +* style: Appease PMD/Codacy +* feat: `ConflictTarget` allows multiple `IndexColumnNames` +* fixes #1749 +* fixes #1633 +* fixes #955 +* doc: fix reference in the Java Doc +* build: better Upload Groovy Task +* --------- +* Co-authored-by: zaza <tzarna@gmail.com> [31ef1](https://github.com/JSQLParser/JSqlParser/commit/31ef1aaf23e2917) manticore-projects *2023-03-21 22:04:58* **disable xml report (#1748)** -* Co-authored-by: other <other@ECE-A55006.austin.utexas.edu> +* Co-authored-by: other <other@ECE-A55006.austin.utexas.edu> [476d9](https://github.com/JSQLParser/JSqlParser/commit/476d96965492131) optimizing-ci-builds *2023-03-21 21:58:25* **Assorted Fixes #6 (#1740)** -* Fixes #1684: Support CREATE MATERIALIZED VIEW with AUTO REFRESH -* Support parsing create view statements in Redshift with AUTO REFRESH -* option. -* Reduce cyclomatic complexity in CreateView.toString -* Extract adding the force option into a dedicated method resulting in the -* cyclomatic complexity reduction of the CreateView.toString method. -* Enhanced Keywords -* Add Keywords and document, which keywords are allowed for what purpose -* Fix incorrect tests -* Define Reserved Keywords explicitly -* Derive All Keywords from Grammar directly -* Generate production for Object Names (semi-) automatically -* Add parametrized Keyword Tests -* Fix test resources -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Sphinx Documentation -* Update the MANTICORE Sphinx Theme, but ignore it in GIT -* Add the content to the Sphinx sites -* Add a Gradle function to derive Stable and Snapshot version from GIT Tags -* Add a Gradle GIT change task -* Add a Gradle sphinx task -* Add a special Test case for illustrating the use of JSQLParser -* doc: request for `Conventional Commit` messages -* feat: make important Classes Serializable -* Implement Serializable for persisting via ObjectOutputStream -* chore: Make Serializable -* doc: Better integration of the RR diagrams -* - apply neutral Sphinx theme -* - insert the RR diagrams into the sphinx sources -* - better documentation on Gradle dependencies -* - link GitHub repository -* Merge -* feat: Oracle Alternative Quoting -* - add support for Oracle Alternative Quoting e.g. `q'(...)'` -* - fixes #1718 -* - add a Logo and FavIcon to the Website -* - document recent changes on Quoting/Escaping -* - add an example on building SQL from Java -* - rework the README.md, promote the Website -* - add Spotless Formatter, using Google Java Style (with Tab=4 Spaces) -* style: Appease PMD/Codacy -* doc: fix the issue template -* - fix the issue template -* - fix the -SNAPSHOT version number -* Update issue templates -* Update issue templates -* feat: Support more Statement Separators -* - `GO` -* - Slash `/` -* - Two empty lines -* feat: FETCH uses EXPRESSION -* - `FETCH` uses `EXPRESSION` instead of SimpleJDBCParameter only -* - Visit/Accept `FETCH` `EXPRESSION` instead of `append` to String -* - Visit/Accept `OFFSET` `EXPRESSION` instead of `append` to String -* - Gradle: remove obsolete/incompatible `jvmArgs` from Test() -* style: apply Spotless -* test: commit missing test -* feat: Unicode CJK Unified Ideographs (Unicode block) -* fixes #1741 -* feat: Unicode CJK Unified Ideographs (Unicode block) -* fixes #1741 -* feat: Functions with nested Attributes -* Supports `SELECT schemaName.f1(arguments).f2(arguments).f3.f4` and similar constructs -* fixes #1742 -* fixes #1050 -* --------- -* Co-authored-by: zaza <tzarna@gmail.com> +* Fixes #1684: Support CREATE MATERIALIZED VIEW with AUTO REFRESH +* Support parsing create view statements in Redshift with AUTO REFRESH +* option. +* Reduce cyclomatic complexity in CreateView.toString +* Extract adding the force option into a dedicated method resulting in the +* cyclomatic complexity reduction of the CreateView.toString method. +* Enhanced Keywords +* Add Keywords and document, which keywords are allowed for what purpose +* Fix incorrect tests +* Define Reserved Keywords explicitly +* Derive All Keywords from Grammar directly +* Generate production for Object Names (semi-) automatically +* Add parametrized Keyword Tests +* Fix test resources +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Sphinx Documentation +* Update the MANTICORE Sphinx Theme, but ignore it in GIT +* Add the content to the Sphinx sites +* Add a Gradle function to derive Stable and Snapshot version from GIT Tags +* Add a Gradle GIT change task +* Add a Gradle sphinx task +* Add a special Test case for illustrating the use of JSQLParser +* doc: request for `Conventional Commit` messages +* feat: make important Classes Serializable +* Implement Serializable for persisting via ObjectOutputStream +* chore: Make Serializable +* doc: Better integration of the RR diagrams +* - apply neutral Sphinx theme +* - insert the RR diagrams into the sphinx sources +* - better documentation on Gradle dependencies +* - link GitHub repository +* Merge +* feat: Oracle Alternative Quoting +* - add support for Oracle Alternative Quoting e.g. `q'(...)'` +* - fixes #1718 +* - add a Logo and FavIcon to the Website +* - document recent changes on Quoting/Escaping +* - add an example on building SQL from Java +* - rework the README.md, promote the Website +* - add Spotless Formatter, using Google Java Style (with Tab=4 Spaces) +* style: Appease PMD/Codacy +* doc: fix the issue template +* - fix the issue template +* - fix the -SNAPSHOT version number +* Update issue templates +* Update issue templates +* feat: Support more Statement Separators +* - `GO` +* - Slash `/` +* - Two empty lines +* feat: FETCH uses EXPRESSION +* - `FETCH` uses `EXPRESSION` instead of SimpleJDBCParameter only +* - Visit/Accept `FETCH` `EXPRESSION` instead of `append` to String +* - Visit/Accept `OFFSET` `EXPRESSION` instead of `append` to String +* - Gradle: remove obsolete/incompatible `jvmArgs` from Test() +* style: apply Spotless +* test: commit missing test +* feat: Unicode CJK Unified Ideographs (Unicode block) +* fixes #1741 +* feat: Unicode CJK Unified Ideographs (Unicode block) +* fixes #1741 +* feat: Functions with nested Attributes +* Supports `SELECT schemaName.f1(arguments).f2(arguments).f3.f4` and similar constructs +* fixes #1742 +* fixes #1050 +* --------- +* Co-authored-by: zaza <tzarna@gmail.com> [adeed](https://github.com/JSQLParser/JSqlParser/commit/adeed5359c65b8f) manticore-projects *2023-03-09 21:22:40* @@ -1443,34 +2315,34 @@ Changelog of JSqlParser. **Sphinx Documentation** -* Update the MANTICORE Sphinx Theme, but ignore it in GIT -* Add the content to the Sphinx sites -* Add a Gradle function to derive Stable and Snapshot version from GIT Tags -* Add a Gradle GIT change task -* Add a Gradle sphinx task -* Add a special Test case for illustrating the use of JSQLParser +* Update the MANTICORE Sphinx Theme, but ignore it in GIT +* Add the content to the Sphinx sites +* Add a Gradle function to derive Stable and Snapshot version from GIT Tags +* Add a Gradle GIT change task +* Add a Gradle sphinx task +* Add a special Test case for illustrating the use of JSQLParser [2ef66](https://github.com/JSQLParser/JSqlParser/commit/2ef6637afffa943) Andreas Reichel *2023-01-21 04:06:00* **Define Reserved Keywords explicitly** -* Derive All Keywords from Grammar directly -* Generate production for Object Names (semi-) automatically -* Add parametrized Keyword Tests +* Derive All Keywords from Grammar directly +* Generate production for Object Names (semi-) automatically +* Add parametrized Keyword Tests [f49e8](https://github.com/JSQLParser/JSqlParser/commit/f49e828fc9c5f2f) Andreas Reichel *2023-01-21 04:05:51* **Adjust Gradle to JUnit 5** -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 [e960a](https://github.com/JSQLParser/JSqlParser/commit/e960a35e591ce07) Andreas Reichel *2023-01-21 04:05:51* **Enhanced Keywords** -* Add Keywords and document, which keywords are allowed for what purpose +* Add Keywords and document, which keywords are allowed for what purpose [b5321](https://github.com/JSQLParser/JSqlParser/commit/b5321d6e8bac588) Andreas Reichel *2023-01-21 04:05:51* @@ -1501,9 +2373,9 @@ Changelog of JSqlParser. **Adjust Gradle to JUnit 5** -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 [2d51a](https://github.com/JSQLParser/JSqlParser/commit/2d51a82d3e9e51c) Andreas Reichel *2023-01-21 04:05:51* @@ -1514,15 +2386,15 @@ Changelog of JSqlParser. **Reduce cyclomatic complexity in CreateView.toString** -* Extract adding the force option into a dedicated method resulting in the -* cyclomatic complexity reduction of the CreateView.toString method. +* Extract adding the force option into a dedicated method resulting in the +* cyclomatic complexity reduction of the CreateView.toString method. [ea447](https://github.com/JSQLParser/JSqlParser/commit/ea4477bb775ebdb) zaza *2023-01-08 20:43:40* **Fixes #1684: Support CREATE MATERIALIZED VIEW with AUTO REFRESH** -* Support parsing create view statements in Redshift with AUTO REFRESH -* option. +* Support parsing create view statements in Redshift with AUTO REFRESH +* option. [74715](https://github.com/JSQLParser/JSqlParser/commit/747152a9fc1bfd1) zaza *2022-12-11 20:03:52* @@ -1531,7 +2403,7 @@ Changelog of JSqlParser. ### Bug Fixes -- add missing public Getter (#1632) ([d2212](https://github.com/JSQLParser/JSqlParser/commit/d2212776ac5eb83) manticore-projects) +- add missing public Getter (#1632) ([d2212](https://github.com/JSQLParser/JSqlParser/commit/d2212776ac5eb83) manticore-projects) ### Other changes @@ -1562,88 +2434,88 @@ Changelog of JSqlParser. **Oracle Alternative Quoting (#1722)** -* Fixes #1684: Support CREATE MATERIALIZED VIEW with AUTO REFRESH -* Support parsing create view statements in Redshift with AUTO REFRESH -* option. -* Reduce cyclomatic complexity in CreateView.toString -* Extract adding the force option into a dedicated method resulting in the -* cyclomatic complexity reduction of the CreateView.toString method. -* Enhanced Keywords -* Add Keywords and document, which keywords are allowed for what purpose -* Fix incorrect tests -* Define Reserved Keywords explicitly -* Derive All Keywords from Grammar directly -* Generate production for Object Names (semi-) automatically -* Add parametrized Keyword Tests -* Fix test resources -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Sphinx Documentation -* Update the MANTICORE Sphinx Theme, but ignore it in GIT -* Add the content to the Sphinx sites -* Add a Gradle function to derive Stable and Snapshot version from GIT Tags -* Add a Gradle GIT change task -* Add a Gradle sphinx task -* Add a special Test case for illustrating the use of JSQLParser -* doc: request for `Conventional Commit` messages -* feat: make important Classes Serializable -* Implement Serializable for persisting via ObjectOutputStream -* chore: Make Serializable -* doc: Better integration of the RR diagrams -* - apply neutral Sphinx theme -* - insert the RR diagrams into the sphinx sources -* - better documentation on Gradle dependencies -* - link GitHub repository -* Merge -* feat: Oracle Alternative Quoting -* - add support for Oracle Alternative Quoting e.g. `q'(...)'` -* - fixes #1718 -* - add a Logo and FavIcon to the Website -* - document recent changes on Quoting/Escaping -* - add an example on building SQL from Java -* - rework the README.md, promote the Website -* - add Spotless Formatter, using Google Java Style (with Tab=4 Spaces) -* style: Appease PMD/Codacy -* doc: fix the issue template -* - fix the issue template -* - fix the -SNAPSHOT version number -* Update issue templates -* Update issue templates -* feat: Support more Statement Separators -* - `GO` -* - Slash `/` -* - Two empty lines -* --------- -* Co-authored-by: zaza <tzarna@gmail.com> +* Fixes #1684: Support CREATE MATERIALIZED VIEW with AUTO REFRESH +* Support parsing create view statements in Redshift with AUTO REFRESH +* option. +* Reduce cyclomatic complexity in CreateView.toString +* Extract adding the force option into a dedicated method resulting in the +* cyclomatic complexity reduction of the CreateView.toString method. +* Enhanced Keywords +* Add Keywords and document, which keywords are allowed for what purpose +* Fix incorrect tests +* Define Reserved Keywords explicitly +* Derive All Keywords from Grammar directly +* Generate production for Object Names (semi-) automatically +* Add parametrized Keyword Tests +* Fix test resources +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Sphinx Documentation +* Update the MANTICORE Sphinx Theme, but ignore it in GIT +* Add the content to the Sphinx sites +* Add a Gradle function to derive Stable and Snapshot version from GIT Tags +* Add a Gradle GIT change task +* Add a Gradle sphinx task +* Add a special Test case for illustrating the use of JSQLParser +* doc: request for `Conventional Commit` messages +* feat: make important Classes Serializable +* Implement Serializable for persisting via ObjectOutputStream +* chore: Make Serializable +* doc: Better integration of the RR diagrams +* - apply neutral Sphinx theme +* - insert the RR diagrams into the sphinx sources +* - better documentation on Gradle dependencies +* - link GitHub repository +* Merge +* feat: Oracle Alternative Quoting +* - add support for Oracle Alternative Quoting e.g. `q'(...)'` +* - fixes #1718 +* - add a Logo and FavIcon to the Website +* - document recent changes on Quoting/Escaping +* - add an example on building SQL from Java +* - rework the README.md, promote the Website +* - add Spotless Formatter, using Google Java Style (with Tab=4 Spaces) +* style: Appease PMD/Codacy +* doc: fix the issue template +* - fix the issue template +* - fix the -SNAPSHOT version number +* Update issue templates +* Update issue templates +* feat: Support more Statement Separators +* - `GO` +* - Slash `/` +* - Two empty lines +* --------- +* Co-authored-by: zaza <tzarna@gmail.com> [e71e5](https://github.com/JSQLParser/JSqlParser/commit/e71e57dfe4b377c) manticore-projects *2023-02-07 20:18:52* **Issue1673 case within brackets (#1675)** -* fix: add missing public Getter -* Add public Getter for `updateSets` -* Fixes #1630 -* fix: Case within brackets -* fixes #1673 +* fix: add missing public Getter +* Add public Getter for `updateSets` +* Fixes #1630 +* fix: Case within brackets +* fixes #1673 [2ced7](https://github.com/JSQLParser/JSqlParser/commit/2ced7ded930f8b0) manticore-projects *2023-01-31 20:56:01* **Added support for SHOW INDEX from table (#1704)** -* Added support for SHOW INDEX from table -* Added * import -* fix for javadoc -* added <doclint>none</doclint> +* Added support for SHOW INDEX from table +* Added * import +* fix for javadoc +* added <doclint>none</doclint> [a2618](https://github.com/JSQLParser/JSqlParser/commit/a2618321135d517) Jayant Kumar Yadav *2023-01-31 20:54:05* @@ -1654,149 +2526,149 @@ Changelog of JSqlParser. **Sphinx Website (#1624)** -* Enhanced Keywords -* Add Keywords and document, which keywords are allowed for what purpose -* Fix incorrect tests -* Define Reserved Keywords explicitly -* Derive All Keywords from Grammar directly -* Generate production for Object Names (semi-) automatically -* Add parametrized Keyword Tests -* Fix test resources -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Keyword test adopt JUnit5 -* Update keywords -* CheckStyle sanitation of method names -* Merge Master -* Add Jupiter Parameters dependency again -* Automate the `updateKeywords` Step -* Update PMD and rules -* Rewrite test expected to fail -* Appease Codacy -* Remove broken rule warning about perfectly fine switch-case statements -* Force Changes -* Fix Merge Issues -* Read Tokens directly from the Grammar File without invoking JTREE -* - read Tokens per REGEX Matcher -* - move Reserved Keywords from Grammar into ParserKeywordsUtils -* - adjust the Tests -* Appease PMD/Codacy -* Extract the Keywords from the Grammar by using JTRee (instead of Regex) -* Add some tests to ensure, that all Keywords or found -* Appease Codacy/PMD -* Separate UpdateKeywords Task again -* Including it into compileJavacc won't work since it depends on compiling the ParserKeywordUtils.java -* Single file compilation did not work -* Clean-up the imports -* Add JavaCC dependency to Maven for building ParserKeywordsUtils -* Add JavaCC dependency to Maven for building ParserKeywordsUtils -* Merge Upstream -* Merge Master -* Fixes broken PR #1524 and Commit fb6e950ce0e62ebcd7a44ba9eea679da2b04b2ed -* Add AST Visualization -* Show the Statement's Java Objects in a tree hierarchy -* Sphinx Documentation -* Update the MANTICORE Sphinx Theme, but ignore it in GIT -* Add the content to the Sphinx sites -* Add a Gradle function to derive Stable and Snapshot version from GIT Tags -* Add a Gradle GIT change task -* Add a Gradle sphinx task -* Add a special Test case for illustrating the use of JSQLParser -* test: Document an additional Special Oracle test success -* doc: ignore the autogenerated changelog.rst in GIT -* build: temporarily reduce the Code Coverage requirements -* Temporarily reduce the Coverage checks regarding Minimum Coverage and Maximum Missed Lines in order to get the Keywords PR accepted. We should do a major Code cleanup afterwards. -* build: Clean-up the Gradle Build -* Prefix the Sphinx Prolog Variables with JSQLPARSER in order to allow for build the Main Website for various projects -* Remove some redundant version requests for PMD, CheckStyle and friends -* Remove JUnit-4 dependency and add HarmCrest -* Complete the PUBLISHING task -* doc: Explain the ``updateKeywords`` Gradle Task -* build: Un-escape the Unicode on the changelog file -* build: Un-escape the Unicode on the changelog file -* doc: Cleanup -* Unescape unicode characters from Git Changelog -* Remove obsolete code from Sphinx' conf.py -* doc: Properly un-escape the Git Commit message -* doc: request for `Conventional Commit` messages -* doc: correctly refer to `RelObjectNameWithoutValue()` -* build: upload the built files via Excec/SFTP -* doc: Add an example on Token White-listing -* doc: write the correct Git Repository -* doc: pronounce the OVERLAPS example more -* feat: make important Classes Serializable -* Implement Serializable for persisting via ObjectOutputStream -* doc: Add the "How to Use" java code -* chore: Make Serializable -* fix: Non-serializable field in serializable class -* build: various fixes to the Maven build file -* add the Keywords Documentation file to the task -* exclude the Sphinx files from the license header plugin -* fix the JavaDoc plugin options -* build: add the Keywords Documentation file to the task -* doc: add a page about actually Reserved Keywords -* build: avoid PMD/Codacy for Sphinx Documentation -* update Changelog -* build: Add Sphinx GitHub Action -* Add a GitHub Action, which will -* - Install Sphinx and Extensions -* - Install Gradle Wrapper -* - Run Gradle Wrapper Task `sphinx` -* - Deploy the generated static HTML site to GH Pages -* fix: fix a merge error, brackets -* fix: remove JavaCC dependency -* Parse Tokens via Regex -* Move JavaCC Token Parser into the KeywordsTest -* Make JavaCC a Test Dependency only -* doc: Fix Maven Artifact Version -* style: Avoid throwing raw exception types. -* style: Avoid throwing raw exception types. -* doc: Better integration of the RR diagrams -* - apply neutral Sphinx theme -* - insert the RR diagrams into the sphinx sources -* - better documentation on Gradle dependencies -* - link GitHub repository -* build: gradle, execute all Checks after Test -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Enhanced Keywords +* Add Keywords and document, which keywords are allowed for what purpose +* Fix incorrect tests +* Define Reserved Keywords explicitly +* Derive All Keywords from Grammar directly +* Generate production for Object Names (semi-) automatically +* Add parametrized Keyword Tests +* Fix test resources +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Keyword test adopt JUnit5 +* Update keywords +* CheckStyle sanitation of method names +* Merge Master +* Add Jupiter Parameters dependency again +* Automate the `updateKeywords` Step +* Update PMD and rules +* Rewrite test expected to fail +* Appease Codacy +* Remove broken rule warning about perfectly fine switch-case statements +* Force Changes +* Fix Merge Issues +* Read Tokens directly from the Grammar File without invoking JTREE +* - read Tokens per REGEX Matcher +* - move Reserved Keywords from Grammar into ParserKeywordsUtils +* - adjust the Tests +* Appease PMD/Codacy +* Extract the Keywords from the Grammar by using JTRee (instead of Regex) +* Add some tests to ensure, that all Keywords or found +* Appease Codacy/PMD +* Separate UpdateKeywords Task again +* Including it into compileJavacc won't work since it depends on compiling the ParserKeywordUtils.java +* Single file compilation did not work +* Clean-up the imports +* Add JavaCC dependency to Maven for building ParserKeywordsUtils +* Add JavaCC dependency to Maven for building ParserKeywordsUtils +* Merge Upstream +* Merge Master +* Fixes broken PR #1524 and Commit fb6e950ce0e62ebcd7a44ba9eea679da2b04b2ed +* Add AST Visualization +* Show the Statement's Java Objects in a tree hierarchy +* Sphinx Documentation +* Update the MANTICORE Sphinx Theme, but ignore it in GIT +* Add the content to the Sphinx sites +* Add a Gradle function to derive Stable and Snapshot version from GIT Tags +* Add a Gradle GIT change task +* Add a Gradle sphinx task +* Add a special Test case for illustrating the use of JSQLParser +* test: Document an additional Special Oracle test success +* doc: ignore the autogenerated changelog.rst in GIT +* build: temporarily reduce the Code Coverage requirements +* Temporarily reduce the Coverage checks regarding Minimum Coverage and Maximum Missed Lines in order to get the Keywords PR accepted. We should do a major Code cleanup afterwards. +* build: Clean-up the Gradle Build +* Prefix the Sphinx Prolog Variables with JSQLPARSER in order to allow for build the Main Website for various projects +* Remove some redundant version requests for PMD, CheckStyle and friends +* Remove JUnit-4 dependency and add HarmCrest +* Complete the PUBLISHING task +* doc: Explain the ``updateKeywords`` Gradle Task +* build: Un-escape the Unicode on the changelog file +* build: Un-escape the Unicode on the changelog file +* doc: Cleanup +* Unescape unicode characters from Git Changelog +* Remove obsolete code from Sphinx' conf.py +* doc: Properly un-escape the Git Commit message +* doc: request for `Conventional Commit` messages +* doc: correctly refer to `RelObjectNameWithoutValue()` +* build: upload the built files via Excec/SFTP +* doc: Add an example on Token White-listing +* doc: write the correct Git Repository +* doc: pronounce the OVERLAPS example more +* feat: make important Classes Serializable +* Implement Serializable for persisting via ObjectOutputStream +* doc: Add the "How to Use" java code +* chore: Make Serializable +* fix: Non-serializable field in serializable class +* build: various fixes to the Maven build file +* add the Keywords Documentation file to the task +* exclude the Sphinx files from the license header plugin +* fix the JavaDoc plugin options +* build: add the Keywords Documentation file to the task +* doc: add a page about actually Reserved Keywords +* build: avoid PMD/Codacy for Sphinx Documentation +* update Changelog +* build: Add Sphinx GitHub Action +* Add a GitHub Action, which will +* - Install Sphinx and Extensions +* - Install Gradle Wrapper +* - Run Gradle Wrapper Task `sphinx` +* - Deploy the generated static HTML site to GH Pages +* fix: fix a merge error, brackets +* fix: remove JavaCC dependency +* Parse Tokens via Regex +* Move JavaCC Token Parser into the KeywordsTest +* Make JavaCC a Test Dependency only +* doc: Fix Maven Artifact Version +* style: Avoid throwing raw exception types. +* style: Avoid throwing raw exception types. +* doc: Better integration of the RR diagrams +* - apply neutral Sphinx theme +* - insert the RR diagrams into the sphinx sources +* - better documentation on Gradle dependencies +* - link GitHub repository +* build: gradle, execute all Checks after Test +* Co-authored-by: Tobias <t.warneke@gmx.net> [be8e7](https://github.com/JSQLParser/JSqlParser/commit/be8e7a8a1d77184) manticore-projects *2023-01-20 21:45:35* **Assorted Fixes #5 (#1715)** -* refactor: Merge REPLACE into UPSERT -* fixes #1706 -* feat: `DROP TEMPORARY TABLE ...` -* fixes #1712 -* build: PMD compliance -* ci: Merge master -* feat: Configurable backslash `\` escaping -* - Enables `\` as escape character in String Literals (beside SQL:2016 compliant `'`) -* - Default is OFF (since its not SQL:2016 compliant) -* - Activate per Parser Feature -* - Fixes #1638 -* - Fixes #1209 -* - Fixes #1173 -* - Fixes #1172 -* - Fixes #832 -* - Fixes #827 -* - Fixes #578 -* BREAKING-CHANGE: Backslash Escaping needs to be activated explicitly or else Backslash won't work as Escape Character. -* style: Checkstyle -* style: remove dead code -* style: PMD compliance -* style: Checkstyle, unused import -* feat: allow `S_CHAR_LITERAL` to break lines -* - fixes #875 +* refactor: Merge REPLACE into UPSERT +* fixes #1706 +* feat: `DROP TEMPORARY TABLE ...` +* fixes #1712 +* build: PMD compliance +* ci: Merge master +* feat: Configurable backslash `\` escaping +* - Enables `\` as escape character in String Literals (beside SQL:2016 compliant `'`) +* - Default is OFF (since its not SQL:2016 compliant) +* - Activate per Parser Feature +* - Fixes #1638 +* - Fixes #1209 +* - Fixes #1173 +* - Fixes #1172 +* - Fixes #832 +* - Fixes #827 +* - Fixes #578 +* BREAKING-CHANGE: Backslash Escaping needs to be activated explicitly or else Backslash won't work as Escape Character. +* style: Checkstyle +* style: remove dead code +* style: PMD compliance +* style: Checkstyle, unused import +* feat: allow `S_CHAR_LITERAL` to break lines +* - fixes #875 [a00d7](https://github.com/JSQLParser/JSqlParser/commit/a00d77a100bfab7) manticore-projects *2023-01-20 21:32:20* @@ -1812,7 +2684,7 @@ Changelog of JSqlParser. **Update README.md** -* lgtm removed +* lgtm removed [954b8](https://github.com/JSQLParser/JSqlParser/commit/954b8dd2e760a01) Tobias *2022-12-27 10:34:18* @@ -1823,69 +2695,69 @@ Changelog of JSqlParser. **Assorted Fixes #4 (#1676)** -* support clickhouse global keyword in join -* fix: add missing public Getter -* Add public Getter for `updateSets` -* Fixes #1630 -* feat: Clickhouse GLOBAL JOIN -* All credits to @julianzlzhang -* fixes #1615 -* fixes #1535 -* feat: IF/ELSE statements supports Block -* Make `If... Else...` statements work with Blocks -* Make `Statement()` production work with `Block()` -* Rewrite the `Block()` related Unit Tests -* fixes #1682 -* fix: Revert unintended changes to the Special Oracle Tests -* fix: `SET` statement supports `UserVariable` -* Make `SetStatement` parse Objects instead of Names only -* Add Grammar to accept `UserVariable` (e.g. "set @Flag = 1") -* Add Test Case for `UserVariable` -* fixes #1682 -* feat: Google Spanner Support -* Replaces PR #1415, all credit goes to @s13o -* Re-arranged some recently added Tokens in alphabetical order -* Update Keywords -* fix: fix JSonExpression, accept Expressions -* Make JSonExpression accept Expressions -* Add Testcase -* Expose Idents() and Operators() -* Fixes #1696 -* test: add Test for Issue #1237 -* Co-authored-by: Zhang Zhongliang <zhangzhongliang@xiaomi.com> +* support clickhouse global keyword in join +* fix: add missing public Getter +* Add public Getter for `updateSets` +* Fixes #1630 +* feat: Clickhouse GLOBAL JOIN +* All credits to @julianzlzhang +* fixes #1615 +* fixes #1535 +* feat: IF/ELSE statements supports Block +* Make `If... Else...` statements work with Blocks +* Make `Statement()` production work with `Block()` +* Rewrite the `Block()` related Unit Tests +* fixes #1682 +* fix: Revert unintended changes to the Special Oracle Tests +* fix: `SET` statement supports `UserVariable` +* Make `SetStatement` parse Objects instead of Names only +* Add Grammar to accept `UserVariable` (e.g. "set @Flag = 1") +* Add Test Case for `UserVariable` +* fixes #1682 +* feat: Google Spanner Support +* Replaces PR #1415, all credit goes to @s13o +* Re-arranged some recently added Tokens in alphabetical order +* Update Keywords +* fix: fix JSonExpression, accept Expressions +* Make JSonExpression accept Expressions +* Add Testcase +* Expose Idents() and Operators() +* Fixes #1696 +* test: add Test for Issue #1237 +* Co-authored-by: Zhang Zhongliang <zhangzhongliang@xiaomi.com> [8d9db](https://github.com/JSQLParser/JSqlParser/commit/8d9db7052c3aeb5) manticore-projects *2022-12-22 21:17:55* **Fixed download war script in the renderRR task (#1659)** -* Co-authored-by: Hai Chang <haichang@microsoft.com> +* Co-authored-by: Hai Chang <haichang@microsoft.com> [08a92](https://github.com/JSQLParser/JSqlParser/commit/08a92fcd7b4f7f2) haha1903 *2022-12-10 09:23:53* **Assorted fixes (#1666)** -* fix: add missing public Getter -* Add public Getter for `updateSets` -* Fixes #1630 -* feat: LISTAGG() with OVER() clause -* fixes issue #1652 -* fixes 3 more Special Oracle Tests -* fix: White-list CURRENT_DATE and CURRENT_TIMESTAMP tokens -* allows CURRENT_DATE(3) and CURRENT_TIMESTAMP(3) as regular functions -* fixes #1507 -* fixes #1607 -* feat: Deparser for Expression Lists -* Visit each Expression of a List instead ExpressionList.toString() -* fixes #1608 -* fix: Lookahead needed +* fix: add missing public Getter +* Add public Getter for `updateSets` +* Fixes #1630 +* feat: LISTAGG() with OVER() clause +* fixes issue #1652 +* fixes 3 more Special Oracle Tests +* fix: White-list CURRENT_DATE and CURRENT_TIMESTAMP tokens +* allows CURRENT_DATE(3) and CURRENT_TIMESTAMP(3) as regular functions +* fixes #1507 +* fixes #1607 +* feat: Deparser for Expression Lists +* Visit each Expression of a List instead ExpressionList.toString() +* fixes #1608 +* fix: Lookahead needed [bff26](https://github.com/JSQLParser/JSqlParser/commit/bff268a7c699947) manticore-projects *2022-11-20 10:06:01* **Fix parsing statements with multidimensional array PR2 (#1665)** -* Fix parsing statements with multidimensional array -* fix: Whitelist LOCKED keyword -* Co-authored-by: Andrei Lisouski <alisousk@akamai.com> +* Fix parsing statements with multidimensional array +* fix: Whitelist LOCKED keyword +* Co-authored-by: Andrei Lisouski <alisousk@akamai.com> [e1865](https://github.com/JSQLParser/JSqlParser/commit/e186588f044753f) manticore-projects *2022-11-20 09:59:26* @@ -1911,83 +2783,83 @@ Changelog of JSqlParser. **Enhanced Keywords (#1382)** -* Enhanced Keywords -* Add Keywords and document, which keywords are allowed for what purpose -* Fix incorrect tests -* Define Reserved Keywords explicitly -* Derive All Keywords from Grammar directly -* Generate production for Object Names (semi-) automatically -* Add parametrized Keyword Tests -* Fix test resources -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Keyword test adopt JUnit5 -* Update keywords -* CheckStyle sanitation of method names -* Merge Master -* Add Jupiter Parameters dependency again -* Automate the `updateKeywords` Step -* Update PMD and rules -* Rewrite test expected to fail -* Appease Codacy -* Remove broken rule warning about perfectly fine switch-case statements -* Force Changes -* Fix Merge Issues -* Read Tokens directly from the Grammar File without invoking JTREE -* - read Tokens per REGEX Matcher -* - move Reserved Keywords from Grammar into ParserKeywordsUtils -* - adjust the Tests -* Appease PMD/Codacy -* Extract the Keywords from the Grammar by using JTRee (instead of Regex) -* Add some tests to ensure, that all Keywords or found -* Appease Codacy/PMD -* Separate UpdateKeywords Task again -* Including it into compileJavacc won't work since it depends on compiling the ParserKeywordUtils.java -* Single file compilation did not work -* Clean-up the imports -* Add JavaCC dependency to Maven for building ParserKeywordsUtils -* Add JavaCC dependency to Maven for building ParserKeywordsUtils -* Merge Upstream -* Merge Master -* Fixes broken PR #1524 and Commit fb6e950ce0e62ebcd7a44ba9eea679da2b04b2ed -* Add AST Visualization -* Show the Statement's Java Objects in a tree hierarchy -* build: temporarily reduce the Code Coverage requirements -* Temporarily reduce the Coverage checks regarding Minimum Coverage and Maximum Missed Lines in order to get the Keywords PR accepted. We should do a major Code cleanup afterwards. -* build: JSQLParser is a build dependency -* chore: Update keywords -* feat: add line count to output +* Enhanced Keywords +* Add Keywords and document, which keywords are allowed for what purpose +* Fix incorrect tests +* Define Reserved Keywords explicitly +* Derive All Keywords from Grammar directly +* Generate production for Object Names (semi-) automatically +* Add parametrized Keyword Tests +* Fix test resources +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Keyword test adopt JUnit5 +* Update keywords +* CheckStyle sanitation of method names +* Merge Master +* Add Jupiter Parameters dependency again +* Automate the `updateKeywords` Step +* Update PMD and rules +* Rewrite test expected to fail +* Appease Codacy +* Remove broken rule warning about perfectly fine switch-case statements +* Force Changes +* Fix Merge Issues +* Read Tokens directly from the Grammar File without invoking JTREE +* - read Tokens per REGEX Matcher +* - move Reserved Keywords from Grammar into ParserKeywordsUtils +* - adjust the Tests +* Appease PMD/Codacy +* Extract the Keywords from the Grammar by using JTRee (instead of Regex) +* Add some tests to ensure, that all Keywords or found +* Appease Codacy/PMD +* Separate UpdateKeywords Task again +* Including it into compileJavacc won't work since it depends on compiling the ParserKeywordUtils.java +* Single file compilation did not work +* Clean-up the imports +* Add JavaCC dependency to Maven for building ParserKeywordsUtils +* Add JavaCC dependency to Maven for building ParserKeywordsUtils +* Merge Upstream +* Merge Master +* Fixes broken PR #1524 and Commit fb6e950ce0e62ebcd7a44ba9eea679da2b04b2ed +* Add AST Visualization +* Show the Statement's Java Objects in a tree hierarchy +* build: temporarily reduce the Code Coverage requirements +* Temporarily reduce the Coverage checks regarding Minimum Coverage and Maximum Missed Lines in order to get the Keywords PR accepted. We should do a major Code cleanup afterwards. +* build: JSQLParser is a build dependency +* chore: Update keywords +* feat: add line count to output [4863e](https://github.com/JSQLParser/JSqlParser/commit/4863eb5a8e30a5d) manticore-projects *2022-10-25 23:15:32* **#1610 Support for SKIP LOCKED tokens on SELECT statements (#1649)** -* Co-authored-by: Lucas Dillmann <lucas.dillmann@totvs.com.br> +* Co-authored-by: Lucas Dillmann <lucas.dillmann@totvs.com.br> [e6d50](https://github.com/JSQLParser/JSqlParser/commit/e6d50f756e99846) Lucas Dillmann *2022-10-25 22:59:09* **Assorted fixes (#1646)** -* fix: add missing public Getter -* Add public Getter for `updateSets` -* Fixes #1630 -* fix: Assorted Fixes -* SelectExpressionItem with Function and Complex Parameters -* Tables with Oracle DB Links -* Make Table Name Parts accessible -* Fixes #1644 -* Fixes #1643 -* fix: Revert correct test case +* fix: add missing public Getter +* Add public Getter for `updateSets` +* Fixes #1630 +* fix: Assorted Fixes +* SelectExpressionItem with Function and Complex Parameters +* Tables with Oracle DB Links +* Make Table Name Parts accessible +* Fixes #1644 +* Fixes #1643 +* fix: Revert correct test case [15ff8](https://github.com/JSQLParser/JSqlParser/commit/15ff84348228278) manticore-projects *2022-10-16 20:15:36* @@ -1998,30 +2870,30 @@ Changelog of JSqlParser. **Bump h2 from 1.4.200 to 2.1.210 (#1639)** -* Bumps [h2](https://github.com/h2database/h2database) from 1.4.200 to 2.1.210. -* - [Release notes](https://github.com/h2database/h2database/releases) -* - [Commits](https://github.com/h2database/h2database/compare/version-1.4.200...version-2.1.210) -* --- -* updated-dependencies: -* - dependency-name: com.h2database:h2 -* dependency-type: direct:development -* ... -* Signed-off-by: dependabot[bot] <support@github.com> -* Signed-off-by: dependabot[bot] <support@github.com> -* Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +* Bumps [h2](https://github.com/h2database/h2database) from 1.4.200 to 2.1.210. +* - [Release notes](https://github.com/h2database/h2database/releases) +* - [Commits](https://github.com/h2database/h2database/compare/version-1.4.200...version-2.1.210) +* --- +* updated-dependencies: +* - dependency-name: com.h2database:h2 +* dependency-type: direct:development +* ... +* Signed-off-by: dependabot[bot] <support@github.com> +* Signed-off-by: dependabot[bot] <support@github.com> +* Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [fc3c4](https://github.com/JSQLParser/JSqlParser/commit/fc3c4cfd6b1eda9) dependabot[bot] *2022-09-28 19:52:31* **Support BigQuery SAFE_CAST (#1622) (#1634)** -* Co-authored-by: Zhang, Dequn <deqzhang@paypal.com> +* Co-authored-by: Zhang, Dequn <deqzhang@paypal.com> [d9985](https://github.com/JSQLParser/JSqlParser/commit/d9985ae4f559cda) dequn *2022-09-20 18:22:25* **Support timestamptz dateliteral (#1621)** -* support timestamptz as datetime literal -* rename test +* support timestamptz as datetime literal +* rename test [81a64](https://github.com/JSQLParser/JSqlParser/commit/81a648eba8db92d) Todd Pollak *2022-08-31 20:31:44* @@ -2042,25 +2914,25 @@ Changelog of JSqlParser. **Fixes PR #1524 support hive alter sql (#1609)** -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Fixes broken PR #1524 and Commit fb6e950ce0e62ebcd7a44ba9eea679da2b04b2ed +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Fixes broken PR #1524 and Commit fb6e950ce0e62ebcd7a44ba9eea679da2b04b2ed [2619c](https://github.com/JSQLParser/JSqlParser/commit/2619ce0a6fd8bd5) manticore-projects *2022-08-14 16:29:18* **#1524 support hive alter sql : ALTER TABLE name ADD COLUMNS (col_spec[, col_spec ...]) (#1605)** -* Co-authored-by: zhum@aotain.com <zm7705264> +* Co-authored-by: zhum@aotain.com <zm7705264> [fb6e9](https://github.com/JSQLParser/JSqlParser/commit/fb6e950ce0e62eb) Zhumin-lv-wn *2022-08-03 20:56:44* @@ -2116,55 +2988,55 @@ Changelog of JSqlParser. **PostgreSQL INSERT ... ON CONFLICT Issue #1551 (#1552)** -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Support Postgres INSERT ... ON CONFLICT -* Fixes #1551 -* Refactor UpdateSet.toString(), which is used by Insert and Update -* Allow KEEP keyword -* Enables special Oracle Test keywordasidentifier04.sql, now 191 tests succeed -* Sanitize before push -* Tweak Grammar in order to survive the Maven Build -* Ammend the README -* Move Plugin configuration files to the CONFIG folder (hoping, that Codacy will find it there) -* Update PMD in the Maven configuration -* Update PMD in the Maven and Gradle configuration -* Appease Codacy -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Support Postgres INSERT ... ON CONFLICT +* Fixes #1551 +* Refactor UpdateSet.toString(), which is used by Insert and Update +* Allow KEEP keyword +* Enables special Oracle Test keywordasidentifier04.sql, now 191 tests succeed +* Sanitize before push +* Tweak Grammar in order to survive the Maven Build +* Ammend the README +* Move Plugin configuration files to the CONFIG folder (hoping, that Codacy will find it there) +* Update PMD in the Maven configuration +* Update PMD in the Maven and Gradle configuration +* Appease Codacy +* Co-authored-by: Tobias <t.warneke@gmx.net> [5ae09](https://github.com/JSQLParser/JSqlParser/commit/5ae09ad097c7294) manticore-projects *2022-07-19 21:18:02* **Configurable Parser Timeout via Feature (#1592)** -* Configurable Parser Timeout via Feature -* Fixes #1582 -* Implement Parser Timeout Feature, e. g. `CCJSqlParserUtil.parse(sqlStr, parser -> parser.withTimeOut(60000));` -* Add a special test failing after a long time only, to test TimeOut vs. Parser Exception -* Appease Codacy -* Appease Codacy -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Configurable Parser Timeout via Feature +* Fixes #1582 +* Implement Parser Timeout Feature, e. g. `CCJSqlParserUtil.parse(sqlStr, parser -> parser.withTimeOut(60000));` +* Add a special test failing after a long time only, to test TimeOut vs. Parser Exception +* Appease Codacy +* Appease Codacy +* Co-authored-by: Tobias <t.warneke@gmx.net> [74000](https://github.com/JSQLParser/JSqlParser/commit/74000130e850788) manticore-projects *2022-07-19 20:48:49* @@ -2180,10 +3052,10 @@ Changelog of JSqlParser. **extended support Postgres' `Extract( field FROM source)` where `field` is a String instead of a Keyword (#1591)** -* Fixes #1582 -* Amend the ExtractExpression -* Add Test case for issue #1582 -* Amend the README +* Fixes #1582 +* Amend the ExtractExpression +* Add Test case for issue #1582 +* Amend the README [2b3ce](https://github.com/JSQLParser/JSqlParser/commit/2b3ce25a23b264a) manticore-projects *2022-07-19 19:25:23* @@ -2214,9 +3086,9 @@ Changelog of JSqlParser. **Closes #1583:: Implement Postgresql optional TABLE in TRUNCATE (#1585)** -* Closes #1583 -* Closes #1583, removed unnecessary local variable. -* Closes #1583, proper support for deparsing. +* Closes #1583 +* Closes #1583, removed unnecessary local variable. +* Closes #1583, proper support for deparsing. [26248](https://github.com/JSQLParser/JSqlParser/commit/262482610b80d18) Rob Audenaerde *2022-07-14 18:55:44* @@ -2227,10 +3099,10 @@ Changelog of JSqlParser. **Support table option character set and index options (#1586)** -* Support table option character set and index options -* Signed-off-by: luofei <luoffei@outlook.com> -* move test -* Signed-off-by: luofei <luoffei@outlook.com> +* Support table option character set and index options +* Signed-off-by: luofei <luoffei@outlook.com> +* move test +* Signed-off-by: luofei <luoffei@outlook.com> [27cdf](https://github.com/JSQLParser/JSqlParser/commit/27cdfa9ca1237f6) luofei *2022-07-14 18:46:14* @@ -2266,8 +3138,8 @@ Changelog of JSqlParser. **Add test for LikeExpression.setEscape and LikeExpression.getStringExpression (#1568)** -* Add test for LikeExpression.setEscape and LikeExpression.getStringExpression -* like + set escape test for $ as escape character +* Add test for LikeExpression.setEscape and LikeExpression.getStringExpression +* like + set escape test for $ as escape character [bcf6f](https://github.com/JSQLParser/JSqlParser/commit/bcf6ff4157277f9) Caro *2022-07-07 19:27:43* @@ -2288,8 +3160,8 @@ Changelog of JSqlParser. **Add support for Hive dialect GROUPING SETS. (#1539)** -* Add support for Hive GROUPING SETS dialect `GROUP BY a, b, c GROUPING SETS ((a, b), (a, c))` -* Simplify HiveTest::testGroupByGroupingSets. +* Add support for Hive GROUPING SETS dialect `GROUP BY a, b, c GROUPING SETS ((a, b), (a, c))` +* Simplify HiveTest::testGroupByGroupingSets. [03c58](https://github.com/JSQLParser/JSqlParser/commit/03c58de9d341a13) chenwl *2022-07-06 19:40:41* @@ -2300,13 +3172,13 @@ Changelog of JSqlParser. **Postgres NATURAL LEFT/RIGHT joins (#1560)** -* Postgres NATURAL LEFT/RIGHT joins -* Fixes #1559 -* Make NATURAL an optional Join Keyword, which can be combined with LEFT, RIGHT, INNER -* Add tests -* Postgres NATURAL LEFT/RIGHT joins -* Amend readme -* Revert successful Oracle test +* Postgres NATURAL LEFT/RIGHT joins +* Fixes #1559 +* Make NATURAL an optional Join Keyword, which can be combined with LEFT, RIGHT, INNER +* Add tests +* Postgres NATURAL LEFT/RIGHT joins +* Amend readme +* Revert successful Oracle test [74a0f](https://github.com/JSQLParser/JSqlParser/commit/74a0f2fb22e24fe) manticore-projects *2022-06-28 20:15:34* @@ -2327,7 +3199,7 @@ Changelog of JSqlParser. **Create maven.yml** -* started maven build using github actions +* started maven build using github actions [b7e5c](https://github.com/JSQLParser/JSqlParser/commit/b7e5c151df37f5e) Tobias *2022-05-16 09:24:24* @@ -2343,40 +3215,40 @@ Changelog of JSqlParser. **INSERT with SetOperations (#1531)** -* INSERT with SetOperations -* Simplify the INSERT production -* Use SetOperations for Select and Values -* Better Bracket handling for WITH ... SELECT ... -* Fixes #1491 -* INSERT with SetOperations -* Appease Codazy/PMD -* INSERT with SetOperations -* Appease Codazy/PMD -* Update Readme -* List the changes -* Minor rephrases -* Correct the Maven Artifact Example -* Fix the two test cases (missing white space) -* Remove unused import +* INSERT with SetOperations +* Simplify the INSERT production +* Use SetOperations for Select and Values +* Better Bracket handling for WITH ... SELECT ... +* Fixes #1491 +* INSERT with SetOperations +* Appease Codazy/PMD +* INSERT with SetOperations +* Appease Codazy/PMD +* Update Readme +* List the changes +* Minor rephrases +* Correct the Maven Artifact Example +* Fix the two test cases (missing white space) +* Remove unused import [b5672](https://github.com/JSQLParser/JSqlParser/commit/b5672c54386cdf8) manticore-projects *2022-05-15 20:29:06* **#1516 rename without column keyword (#1533)** -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* `RENAME ... TO ...` without `COLUMN` keyword -* Fixes #1516 +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* `RENAME ... TO ...` without `COLUMN` keyword +* Fixes #1516 [e8f07](https://github.com/JSQLParser/JSqlParser/commit/e8f0750d75e74c7) manticore-projects *2022-05-11 20:44:34* @@ -2392,16 +3264,16 @@ Changelog of JSqlParser. **#1527 DELETE ... RETURNING ... (#1528)** -* #1527 DELETE ... RETURNING ... -* Fixes #1527 -* Add DELETE... RETURNING ... expression -* Simplify INSERT ... RETURNING ... expression -* Simply UPDATE ... RETURNING ... expression -* TSQL Output Clause -* According to https://docs.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql?view=sql-server-ver15 -* Implement Output Clause for INSERT, UPDATE and DELETE -* Add Tests according the Microsoft Documentation -* Appease Codacy/PMD +* #1527 DELETE ... RETURNING ... +* Fixes #1527 +* Add DELETE... RETURNING ... expression +* Simplify INSERT ... RETURNING ... expression +* Simply UPDATE ... RETURNING ... expression +* TSQL Output Clause +* According to https://docs.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql?view=sql-server-ver15 +* Implement Output Clause for INSERT, UPDATE and DELETE +* Add Tests according the Microsoft Documentation +* Appease Codacy/PMD [4d815](https://github.com/JSQLParser/JSqlParser/commit/4d8152159454069) manticore-projects *2022-05-11 20:04:23* @@ -2417,37 +3289,37 @@ Changelog of JSqlParser. **Unsupported statement (#1519)** -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Implement UnsupportedStatement -* - Add Feature allowUnsupportedStatement, default=false -* - Fully implement UnsupportedStatement for the Statement() production -* - Partially implement UnsupportedStatement for the Statements() production, works only when UnsupportedStatement comes first -* Revert unintended changes of the test resources -* Reformat BLOCK production -* Disable STATEMENTS() test, which will never fail and add comments to this regard +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Implement UnsupportedStatement +* - Add Feature allowUnsupportedStatement, default=false +* - Fully implement UnsupportedStatement for the Statement() production +* - Partially implement UnsupportedStatement for the Statements() production, works only when UnsupportedStatement comes first +* Revert unintended changes of the test resources +* Reformat BLOCK production +* Disable STATEMENTS() test, which will never fail and add comments to this regard [59bb9](https://github.com/JSQLParser/JSqlParser/commit/59bb9a4e40753cf) manticore-projects *2022-05-11 19:23:35* @@ -2458,8 +3330,8 @@ Changelog of JSqlParser. **Update bug_report.md (#1512)** -* Focus more on the particular SQL Statement and the JSQLParser Version. -* Link to the Online Formatter for testing. +* Focus more on the particular SQL Statement and the JSQLParser Version. +* Link to the Online Formatter for testing. [13441](https://github.com/JSQLParser/JSqlParser/commit/13441f47fbd8023) manticore-projects *2022-04-22 22:29:07* @@ -2470,40 +3342,40 @@ Changelog of JSqlParser. **Performance Improvements (#1439)** -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Performance Improvements -* Simplify the Primary Expression Production -* Try to simple parse without Complex Expressions first, before parsing complex and slow (if supported by max nesting depth) -* Add Test cases for issues #1397 and #1438 -* Update Libraries to its latest version -* Remove JUnit 4 from Gradle -* Appease PMD -* Update Gradle Plugins to its latest versions -* Let Parser timeout after 6 seconds and fail gently -* Add a special test verifying the clean up after timeout -* Revert unintended changes to the Test Resources -* Appease PMD/Codacy -* Correct the Gradle "+" dependencies -* Bump version to 4.4.-SNAPSHOT -* update build file -* revert unwarranted changes in test files -* strip the Exception Class Name from the Message -* maxDepth = 10 collides with the Parser Timeout = 6 seconds -* License Headers -* Unused imports -* Bump version to 4.5-SNAPSHOT -* Reduce test loops to fit intothe timeout +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Performance Improvements +* Simplify the Primary Expression Production +* Try to simple parse without Complex Expressions first, before parsing complex and slow (if supported by max nesting depth) +* Add Test cases for issues #1397 and #1438 +* Update Libraries to its latest version +* Remove JUnit 4 from Gradle +* Appease PMD +* Update Gradle Plugins to its latest versions +* Let Parser timeout after 6 seconds and fail gently +* Add a special test verifying the clean up after timeout +* Revert unintended changes to the Test Resources +* Appease PMD/Codacy +* Correct the Gradle "+" dependencies +* Bump version to 4.4.-SNAPSHOT +* update build file +* revert unwarranted changes in test files +* strip the Exception Class Name from the Message +* maxDepth = 10 collides with the Parser Timeout = 6 seconds +* License Headers +* Unused imports +* Bump version to 4.5-SNAPSHOT +* Reduce test loops to fit intothe timeout [181a2](https://github.com/JSQLParser/JSqlParser/commit/181a21ab90870e1) manticore-projects *2022-04-14 21:18:18* @@ -2519,31 +3391,31 @@ Changelog of JSqlParser. **Json function Improvements (#1506)** -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Improve JSON Functions -* Space around the `:` delimiter of JSON Functions -* Improve JSON Functions -* Enforce `KEY` as `S_CHAR_LITERAL` -* Allow `Column` as `VALUE` -* Temporarily disable Postgres Syntax -* Improve JSON Functions -* Bring back Postgres Syntax -* Enable MySQL Syntax JSON_OBJECT(key, value [, key, value, ...]) -* Fix some more tests, where key was not a String -* Appease Codacy -* Let JSON_OBJECT accept Expressions as value -* set Version = 4.4-SNAPSHOT +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Improve JSON Functions +* Space around the `:` delimiter of JSON Functions +* Improve JSON Functions +* Enforce `KEY` as `S_CHAR_LITERAL` +* Allow `Column` as `VALUE` +* Temporarily disable Postgres Syntax +* Improve JSON Functions +* Bring back Postgres Syntax +* Enable MySQL Syntax JSON_OBJECT(key, value [, key, value, ...]) +* Fix some more tests, where key was not a String +* Appease Codacy +* Let JSON_OBJECT accept Expressions as value +* set Version = 4.4-SNAPSHOT [e3f53](https://github.com/JSQLParser/JSqlParser/commit/e3f531caf7ad9ba) manticore-projects *2022-04-09 22:37:36* @@ -2564,25 +3436,25 @@ Changelog of JSqlParser. **Issue1500 - Circular References in `AllColumns` and `AllTableColumns` (#1501)** -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports -* Remove circular reference revealed by issue #1500 -* Add test for Issue 1500 Circular reference for All Columns Expression -* Fix Test case -* Add Test for AllTableColumn due to similar circular reference -* Remove similar circular reference from AllTableColumn -* Update dependencies -* Adjust Jacoco Missed Lines count +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports +* Remove circular reference revealed by issue #1500 +* Add test for Issue 1500 Circular reference for All Columns Expression +* Fix Test case +* Add Test for AllTableColumn due to similar circular reference +* Remove similar circular reference from AllTableColumn +* Update dependencies +* Adjust Jacoco Missed Lines count [0949d](https://github.com/JSQLParser/JSqlParser/commit/0949df9d789123c) manticore-projects *2022-04-03 18:51:35* @@ -2593,26 +3465,26 @@ Changelog of JSqlParser. **Optimize assertCanBeParsedAndDeparsed (#1389)** -* Optimize assertCanBeParsedAndDeparsed -* - Avoid redundant calls of buildSqlString() -* - Replace String.replaceAll() with Matcher.replaceAll() based on precompiled Regex Patterns -* Reset the testcase results +* Optimize assertCanBeParsedAndDeparsed +* - Avoid redundant calls of buildSqlString() +* - Replace String.replaceAll() with Matcher.replaceAll() based on precompiled Regex Patterns +* Reset the testcase results [ea316](https://github.com/JSQLParser/JSqlParser/commit/ea3164a1e418f3b) manticore-projects *2022-04-02 22:40:09* **Add geometry distance operator (#1493)** -* Add support for geometry distance operators in PostGIS. -* Fix missing imports. -* Co-authored-by: Thomas Powell <tpowell@palantir.com> +* Add support for geometry distance operators in PostGIS. +* Fix missing imports. +* Co-authored-by: Thomas Powell <tpowell@palantir.com> [98c47](https://github.com/JSQLParser/JSqlParser/commit/98c476a6c9fa1a1) Thomas Powell *2022-04-02 22:31:08* **Support WITH TIES option in TOP #1435 (#1479)** -* Support WITH TIES option in TOP -* - Add the support of WITH TIES option in SELECT TOP statement. -* add specific test +* Support WITH TIES option in TOP +* - Add the support of WITH TIES option in SELECT TOP statement. +* add specific test [1756a](https://github.com/JSQLParser/JSqlParser/commit/1756adcade48fb4) Olivier Cavadenti *2022-04-02 21:26:54* @@ -2628,10 +3500,10 @@ Changelog of JSqlParser. **Extending CaseExpression, covering #1458 (#1459)** -* Add unit tests for Case expressions. -* More tests for CaseExpression. -* Switch expression becomes an Expression instead of a Condition. -* It allows complex expressions in the switch, similarly to what is allowed in when clauses. +* Add unit tests for Case expressions. +* More tests for CaseExpression. +* Switch expression becomes an Expression instead of a Condition. +* It allows complex expressions in the switch, similarly to what is allowed in when clauses. [4df13](https://github.com/JSQLParser/JSqlParser/commit/4df1391a28a7402) Mathieu Goeminne *2022-03-15 20:07:43* @@ -2652,7 +3524,7 @@ Changelog of JSqlParser. **Add support for IS DISTINCT FROM clause (#1457)** -* Co-authored-by: Tomer Shay <tomer@Tomers-MBP.lan> +* Co-authored-by: Tomer Shay <tomer@Tomers-MBP.lan> [31ed3](https://github.com/JSQLParser/JSqlParser/commit/31ed383ff0f3903) Tomer Shay (Shimshi) *2022-01-18 07:01:14* @@ -2688,12 +3560,12 @@ Changelog of JSqlParser. **Adjust Gradle to JUnit 5 (#1428)** -* Adjust Gradle to JUnit 5 -* Parallel Test execution -* Gradle Caching -* Explicitly request for latest JavaCC 7.0.10 -* Do not mark SpeedTest for concurrent execution -* Remove unused imports +* Adjust Gradle to JUnit 5 +* Parallel Test execution +* Gradle Caching +* Explicitly request for latest JavaCC 7.0.10 +* Do not mark SpeedTest for concurrent execution +* Remove unused imports [af7bc](https://github.com/JSQLParser/JSqlParser/commit/af7bc1cc06700c3) manticore-projects *2021-11-28 21:43:10* @@ -2719,7 +3591,7 @@ Changelog of JSqlParser. **Support EMIT CHANGES for KSQL (#1426)** -* - Add the EMIT CHANGES syntax used in KSQL. +* - Add the EMIT CHANGES syntax used in KSQL. [f6c17](https://github.com/JSQLParser/JSqlParser/commit/f6c17412accdd18) Olivier Cavadenti *2021-11-21 12:20:56* @@ -2750,13 +3622,13 @@ Changelog of JSqlParser. **Support RESTART without value (#1425)** -* Since Postgre 8.4, RESTART in ALTER SEQUENCE can be set without value. +* Since Postgre 8.4, RESTART in ALTER SEQUENCE can be set without value. [98b66](https://github.com/JSQLParser/JSqlParser/commit/98b66be4b2919df) Olivier Cavadenti *2021-11-20 00:00:32* **Add support for oracle UnPivot when use multi columns at once. (#1419)** -* Co-authored-by: LeiJun <02280245@yto.net.cn> +* Co-authored-by: LeiJun <02280245@yto.net.cn> [8e8bb](https://github.com/JSQLParser/JSqlParser/commit/8e8bb708636e6c6) LeiJun *2021-11-19 23:22:29* @@ -2767,10 +3639,10 @@ Changelog of JSqlParser. **Fix issue in parsing TRY_CAST() function (#1391)** -* Fix issue in parsing TRY_CAST() function -* Fix issue in parsing TRY_CAST() function -* Add parser, deparser, validator and vistior implementation for try_cast function -* Update toString() method of TryCastExpression class +* Fix issue in parsing TRY_CAST() function +* Fix issue in parsing TRY_CAST() function +* Add parser, deparser, validator and vistior implementation for try_cast function +* Update toString() method of TryCastExpression class [bfcf0](https://github.com/JSQLParser/JSqlParser/commit/bfcf00f9dfcc0a3) Prashant Sutar *2021-11-19 22:24:49* @@ -2781,27 +3653,27 @@ Changelog of JSqlParser. **Add support for expressions (such as columns) in AT TIME ZONE expressions (#1413)** -* Co-authored-by: EverSQL <tomer@eversql.com> +* Co-authored-by: EverSQL <tomer@eversql.com> [ebe17](https://github.com/JSQLParser/JSqlParser/commit/ebe171b3b502089) Tomer Shay (Shimshi) *2021-11-19 22:04:40* **Add supported for quoted cast expressions for PostgreSQL (#1411)** -* Co-authored-by: EverSQL <tomer@eversql.com> +* Co-authored-by: EverSQL <tomer@eversql.com> [dbbce](https://github.com/JSQLParser/JSqlParser/commit/dbbcebbf0490e1c) Tomer Shay (Shimshi) *2021-11-19 21:54:37* **added USE SCHEMA and CREATE OR REPLACE support; things that are allowed in Snowflake SQL (#1409)** -* Co-authored-by: Richard Kooijman <richard.kooijman@inergy.nl> +* Co-authored-by: Richard Kooijman <richard.kooijman@inergy.nl> [f35d2](https://github.com/JSQLParser/JSqlParser/commit/f35d24cfbb88342) Richard Kooijman *2021-11-19 21:40:45* **Issue #420 Like Expression with Escape Expression (#1406)** -* Issue #420 Like Expression with Escape Expression -* Fixes issue #420 -* CheckStyle compliance +* Issue #420 Like Expression with Escape Expression +* Fixes issue #420 +* CheckStyle compliance [8eaa4](https://github.com/JSQLParser/JSqlParser/commit/8eaa4d2fc243f0a) manticore-projects *2021-11-19 21:38:54* @@ -2822,136 +3694,136 @@ Changelog of JSqlParser. **Add Delete / Update modifiers for MySQL #1254 (#1396)** -* Add Delete / Update modifiers for MySQL #1254 -* fix codacy issues + pr return -* simplify low_priority +* Add Delete / Update modifiers for MySQL #1254 +* fix codacy issues + pr return +* simplify low_priority [7be5d](https://github.com/JSQLParser/JSqlParser/commit/7be5d8e65e23f6e) Olivier Cavadenti *2021-11-19 20:31:00* **Fixes #1381 (#1383)** -* Allow Complex Expressions as SelectItem +* Allow Complex Expressions as SelectItem [cdf0f](https://github.com/JSQLParser/JSqlParser/commit/cdf0f095294b04a) manticore-projects *2021-11-19 20:27:44* **Allows CASE ... ELSE ComplexExpression (#1388)** -* Fixes #1375 -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Fixes #1375 +* Co-authored-by: Tobias <t.warneke@gmx.net> [60a7d](https://github.com/JSQLParser/JSqlParser/commit/60a7d103853cb5e) manticore-projects *2021-11-02 20:48:39* **IN() with complex expressions (#1384)** -* IN() with Complex Expressions -* Fixes #905 -* Allow Complex Expressions and multiple SubSelects for the IN() Expression -* Tune the Test Coverage -* Remove unused import -* Reset TEST status +* IN() with Complex Expressions +* Fixes #905 +* Allow Complex Expressions and multiple SubSelects for the IN() Expression +* Tune the Test Coverage +* Remove unused import +* Reset TEST status [c4232](https://github.com/JSQLParser/JSqlParser/commit/c42322440d5fb36) manticore-projects *2021-11-01 21:23:48* **Fixes #1385 and PR#1380 (#1386)** -* Add another Alias() Keyword related LOOKAHEAD -* Fix a Keyword Spelling Error in the Deparser -* Remove UNPIVOT from the PARENTHESIS Deparser, as it was an ugly workaround made obsolete by PR #1380 +* Add another Alias() Keyword related LOOKAHEAD +* Fix a Keyword Spelling Error in the Deparser +* Remove UNPIVOT from the PARENTHESIS Deparser, as it was an ugly workaround made obsolete by PR #1380 [8c1eb](https://github.com/JSQLParser/JSqlParser/commit/8c1eba24be61cf0) manticore-projects *2021-10-22 20:19:15* **Fixes #1369 (#1370)** -* Issue1369 -* Add test +* Issue1369 +* Add test [2335e](https://github.com/JSQLParser/JSqlParser/commit/2335ed136e92163) Ben Grabham *2021-10-20 21:44:06* **Fixes #1371 (#1377)** -* Fixes #1371 -* Postgres specific JSON_OBJECT syntax supporting: -* SELECT json_object('{a, 1, b, 2}'); -* SELECT json_object('{{a, 1}, {b, 2}}'); -* SELECT json_object('{a, b}', '{1,2 }'); -* Improve Test Coverage +* Fixes #1371 +* Postgres specific JSON_OBJECT syntax supporting: +* SELECT json_object('{a, 1, b, 2}'); +* SELECT json_object('{{a, 1}, {b, 2}}'); +* SELECT json_object('{a, b}', '{1,2 }'); +* Improve Test Coverage [cbffe](https://github.com/JSQLParser/JSqlParser/commit/cbffe6b58cd0074) manticore-projects *2021-10-20 21:13:54* **LIMIT OFFSET with Expressions (#1378)** -* Fixes #933 +* Fixes #933 [a52db](https://github.com/JSQLParser/JSqlParser/commit/a52db54ff61be34) manticore-projects *2021-10-20 21:05:27* **Oracle Multi Column Drop (#1379)** -* Fixes #1363 +* Fixes #1363 [9ad18](https://github.com/JSQLParser/JSqlParser/commit/9ad18d29efb66a2) manticore-projects *2021-10-20 21:01:04* **Support alias for UnPivot statement (see discussion #1374) (#1380)** -* - Changed JSqlParserCC.jjt file to add the alias to the UnPivot lexical entity. -* - Added Alias to the UnPivot object. -* - Improved SelectDeParser to correctly deparse SubSelect's UnPivot component. +* - Changed JSqlParserCC.jjt file to add the alias to the UnPivot lexical entity. +* - Added Alias to the UnPivot object. +* - Improved SelectDeParser to correctly deparse SubSelect's UnPivot component. [0c0c3](https://github.com/JSQLParser/JSqlParser/commit/0c0c32e9cda0f1a) fabriziodelfranco *2021-10-20 20:18:13* **Issue1352 (#1353)** -* Fixes #1352 -* Allow SYSTEM as table- or column- name -* Fixes #1352 -* Allow SYSTEM as tablename -* Fixes #1352 -* Allow SYSTEM as tablename and columnname -* Fixes #1352 -* Allow QUERY as tablename and columnname -* Fixes #1352 -* Allow FULLTEXT as tablename and columnname -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Fixes #1352 +* Allow SYSTEM as table- or column- name +* Fixes #1352 +* Allow SYSTEM as tablename +* Fixes #1352 +* Allow SYSTEM as tablename and columnname +* Fixes #1352 +* Allow QUERY as tablename and columnname +* Fixes #1352 +* Allow FULLTEXT as tablename and columnname +* Co-authored-by: Tobias <t.warneke@gmx.net> [a8afd](https://github.com/JSQLParser/JSqlParser/commit/a8afd9a4e6a8bc0) manticore-projects *2021-10-09 21:31:45* **Enhance ALTER TABLE ... DROP CONSTRAINTS ... (#1351)** -* Enhance ALTER TABLE ... DROP CONSTRAINTS ... -* Add support for DROP PRIMARY KEY, DROP UNIQUE(...) -* Add support for DROP FOREIGN KEY(...) -* Fixes #1342 -* Remove one useless PMD rule -* Add more tests -* Adjust Test Coverage +* Enhance ALTER TABLE ... DROP CONSTRAINTS ... +* Add support for DROP PRIMARY KEY, DROP UNIQUE(...) +* Add support for DROP FOREIGN KEY(...) +* Fixes #1342 +* Remove one useless PMD rule +* Add more tests +* Adjust Test Coverage [388b7](https://github.com/JSQLParser/JSqlParser/commit/388b7c3afff4f50) manticore-projects *2021-10-08 23:02:46* **Function to use AllColumns or AllTableColumns Expression (#1350)** -* Fix a trivial MERGE error from Commit 4797a8d676625fcc6cf8c9e3b403ca120b6a8141 -* Function use AllColumns or AllTableColumns -* Fixes #1346 -* Remove one useless PMD rule +* Fix a trivial MERGE error from Commit 4797a8d676625fcc6cf8c9e3b403ca120b6a8141 +* Function use AllColumns or AllTableColumns +* Fixes #1346 +* Remove one useless PMD rule [b0ada](https://github.com/JSQLParser/JSqlParser/commit/b0adaa8de1421ea) manticore-projects *2021-10-08 21:58:04* **Postgres compliant ALTER TABLE ... RENAME TO ... (#1334)** -* Fix a trivial MERGE error from Commit 4797a8d676625fcc6cf8c9e3b403ca120b6a8141 -* Fixes #1333 -* Postgres compliant ALTER TABLE ... RENAME TO ... -* Postgres compliant ALTER TABLE IF EXISTS ... RENAME TO ... -* Postgres compliant ALTER TABLE IF EXISTS ... RENAME TO ... +* Fix a trivial MERGE error from Commit 4797a8d676625fcc6cf8c9e3b403ca120b6a8141 +* Fixes #1333 +* Postgres compliant ALTER TABLE ... RENAME TO ... +* Postgres compliant ALTER TABLE IF EXISTS ... RENAME TO ... +* Postgres compliant ALTER TABLE IF EXISTS ... RENAME TO ... [f353e](https://github.com/JSQLParser/JSqlParser/commit/f353ec830deb719) manticore-projects *2021-09-18 11:35:17* **Postgres compliant ALTER TABLE ... RENAME TO ... (#1334)** -* Fix a trivial MERGE error from Commit 4797a8d676625fcc6cf8c9e3b403ca120b6a8141 -* Fixes #1333 -* Postgres compliant ALTER TABLE ... RENAME TO ... -* Postgres compliant ALTER TABLE IF EXISTS ... RENAME TO ... -* Postgres compliant ALTER TABLE IF EXISTS ... RENAME TO ... +* Fix a trivial MERGE error from Commit 4797a8d676625fcc6cf8c9e3b403ca120b6a8141 +* Fixes #1333 +* Postgres compliant ALTER TABLE ... RENAME TO ... +* Postgres compliant ALTER TABLE IF EXISTS ... RENAME TO ... +* Postgres compliant ALTER TABLE IF EXISTS ... RENAME TO ... [5e904](https://github.com/JSQLParser/JSqlParser/commit/5e9045f431d9cc4) manticore-projects *2021-09-18 11:34:50* @@ -2982,18 +3854,18 @@ Changelog of JSqlParser. **Prepare4.2 (#1329)** -* Implement caching of the Gradle and Maven files -* Provided by @YunLemon via PR #1307 -* Fix CREATE TABLE AS SELECT ... UNION SELECT ... -* Provided by @fanchuo via PR #1309 -* Fix #1316 -* Add more specific tests verifying the nature of the UpdateSets -* Allow "SELECT *" (without FROM) to parse, its a valid SELECT statement -* Add the enhancements since Release 4.1 -* Adjust the Coverage -* Improve Test Coverage -* Revert the Special Oracle Tests (accidentally set to FAILURE) -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Implement caching of the Gradle and Maven files +* Provided by @YunLemon via PR #1307 +* Fix CREATE TABLE AS SELECT ... UNION SELECT ... +* Provided by @fanchuo via PR #1309 +* Fix #1316 +* Add more specific tests verifying the nature of the UpdateSets +* Allow "SELECT *" (without FROM) to parse, its a valid SELECT statement +* Add the enhancements since Release 4.1 +* Adjust the Coverage +* Improve Test Coverage +* Revert the Special Oracle Tests (accidentally set to FAILURE) +* Co-authored-by: Tobias <t.warneke@gmx.net> [4797a](https://github.com/JSQLParser/JSqlParser/commit/4797a8d676625fc) manticore-projects *2021-09-07 08:56:12* @@ -3004,38 +3876,38 @@ Changelog of JSqlParser. **Fixes #1325 (#1327)** -* Removes redundant production Identifier() and uses RelObjectnameWithoutValue() instead for MS SQL Server Hints +* Removes redundant production Identifier() and uses RelObjectnameWithoutValue() instead for MS SQL Server Hints [cf0d7](https://github.com/JSQLParser/JSqlParser/commit/cf0d74f6572d6aa) manticore-projects *2021-09-06 12:09:01* **Implement Joins with multiple trailing ON Expressions (#1303)** -* Implement Joins with multiple trailing ON Expressions -* Fixes #1302 -* Fixes SpecialOracleTest JOIN17, now 190/273 tests pass -* Fixes #1229 -* Merge MASTER -* Refactor the appendTo() method in favour of the traditional toString() -* Remove unused imports +* Implement Joins with multiple trailing ON Expressions +* Fixes #1302 +* Fixes SpecialOracleTest JOIN17, now 190/273 tests pass +* Fixes #1229 +* Merge MASTER +* Refactor the appendTo() method in favour of the traditional toString() +* Remove unused imports [d18c5](https://github.com/JSQLParser/JSqlParser/commit/d18c59bf845c57d) manticore-projects *2021-09-06 11:34:05* **Fix Gradle PMD and Checkstyle (#1318)** -* Fixes #1306 -* Nested Cases with Complex Expressions -* Reduce coverage for Java 8 -* GROUP BY with Complex Expressions -* Fixes #1308 -* Update Sets with Complex Expressions -* Fixes #1316 -* Update Sets with Complex Expressions -* Fix existing tests -* Add tests for the new functionality -* Implement PMD/Codacy recommendations -* Add Checkstyle Configuration to Gradle -* Add Checkstyle Config files -* Fix additional exceptions in Test Sources +* Fixes #1306 +* Nested Cases with Complex Expressions +* Reduce coverage for Java 8 +* GROUP BY with Complex Expressions +* Fixes #1308 +* Update Sets with Complex Expressions +* Fixes #1316 +* Update Sets with Complex Expressions +* Fix existing tests +* Add tests for the new functionality +* Implement PMD/Codacy recommendations +* Add Checkstyle Configuration to Gradle +* Add Checkstyle Config files +* Fix additional exceptions in Test Sources [2e876](https://github.com/JSQLParser/JSqlParser/commit/2e876130b46d087) manticore-projects *2021-09-01 22:01:40* @@ -3046,27 +3918,27 @@ Changelog of JSqlParser. **Fixes #1306 (#1311)** -* Fixes #1306 -* Nested Cases with Complex Expressions -* Reduce coverage for Java 8 -* GROUP BY with Complex Expressions -* Fixes #1308 +* Fixes #1306 +* Nested Cases with Complex Expressions +* Reduce coverage for Java 8 +* GROUP BY with Complex Expressions +* Fixes #1308 [8f632](https://github.com/JSQLParser/JSqlParser/commit/8f632b92e511b28) manticore-projects *2021-08-28 20:28:55* **Update sets (#1317)** -* Fixes #1306 -* Nested Cases with Complex Expressions -* Reduce coverage for Java 8 -* GROUP BY with Complex Expressions -* Fixes #1308 -* Update Sets with Complex Expressions -* Fixes #1316 -* Update Sets with Complex Expressions -* Fix existing tests -* Add tests for the new functionality -* Implement PMD/Codacy recommendations +* Fixes #1306 +* Nested Cases with Complex Expressions +* Reduce coverage for Java 8 +* GROUP BY with Complex Expressions +* Fixes #1308 +* Update Sets with Complex Expressions +* Fixes #1316 +* Update Sets with Complex Expressions +* Fix existing tests +* Add tests for the new functionality +* Implement PMD/Codacy recommendations [21e5e](https://github.com/JSQLParser/JSqlParser/commit/21e5ebac02822e2) manticore-projects *2021-08-27 21:37:05* @@ -3077,46 +3949,46 @@ Changelog of JSqlParser. **Special oracle tests (#1279)** -* Allow keywords: LINK, GROUPING() -* Deparse ParenthesisFromItem's Pivot and UnPivot correctly -* Write Test results to the SQL File -* Reduce the noise during the test -* Update/correct the list of expected passing files -* Get the benchmark from the list of expected passing files -* There are no Pivots or UnPivots yet, so we will return NULL. -* Record the expected Test Results on each SQL Source -* Fail the test when any expected success suddenly fails -* Improve Test Coverage -* Appease Codacy +* Allow keywords: LINK, GROUPING() +* Deparse ParenthesisFromItem's Pivot and UnPivot correctly +* Write Test results to the SQL File +* Reduce the noise during the test +* Update/correct the list of expected passing files +* Get the benchmark from the list of expected passing files +* There are no Pivots or UnPivots yet, so we will return NULL. +* Record the expected Test Results on each SQL Source +* Fail the test when any expected success suddenly fails +* Improve Test Coverage +* Appease Codacy [346ee](https://github.com/JSQLParser/JSqlParser/commit/346eea5fbcf2461) manticore-projects *2021-08-09 21:55:00* **Implements Hierarchical CONNECT_BY_ROOT Operator (#1282)** -* Implements Hierarchical CONNECT_BY_ROOT Operator -* Fixes Issue #1269 -* Resolves some Special Oracle Tests -* Improve Test Coverage -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Implements Hierarchical CONNECT_BY_ROOT Operator +* Fixes Issue #1269 +* Resolves some Special Oracle Tests +* Improve Test Coverage +* Co-authored-by: Tobias <t.warneke@gmx.net> [b6014](https://github.com/JSQLParser/JSqlParser/commit/b60140740aab93e) manticore-projects *2021-08-09 21:53:08* **Implement Transact-SQL IF ELSE Statement Control Flows. (#1275)** -* Implement Transact-SQL IF ELSE Statement Control Flows. -* Fixes #1273 except for Blocks. -* Improce Test Coverage -* Adjust the required Test Coverage for JDK 8 +* Implement Transact-SQL IF ELSE Statement Control Flows. +* Fixes #1273 except for Blocks. +* Improce Test Coverage +* Adjust the required Test Coverage for JDK 8 [750c3](https://github.com/JSQLParser/JSqlParser/commit/750c30aafe83957) manticore-projects *2021-08-09 21:43:50* **Add some flexibility to the Alter Statement (#1293)** -* in order to allow: -* ALTER TABLE ... MOVE TABLESPACE ... -* ALTER TABLE ... COMPRESS NOLOGGING -* ALTER TABLE ... ROWFORMAT=DYNAMIC -* Fixes #1033 +* in order to allow: +* ALTER TABLE ... MOVE TABLESPACE ... +* ALTER TABLE ... COMPRESS NOLOGGING +* ALTER TABLE ... ROWFORMAT=DYNAMIC +* Fixes #1033 [a88e9](https://github.com/JSQLParser/JSqlParser/commit/a88e921970c57b8) manticore-projects *2021-08-02 20:51:19* @@ -3127,18 +3999,18 @@ Changelog of JSqlParser. **Implement Oracle Named Function Parameters Func( param1 => arg1, ...) (#1283)** -* Fixes #1270 +* Fixes #1270 [c8a5d](https://github.com/JSQLParser/JSqlParser/commit/c8a5d7c3dfc97f4) manticore-projects *2021-08-02 20:32:41* **Implement Gradle Buildsystem (#1271)** -* Gradle build -* implement SpotBugs, PMD and JaCoCo -* implement RR diagrams -* Move Special Oracle Test resources into the correct package -* Implement a basic Gradle/Maven compatibility workaround for the Special Oracle Test -* Fix the Gradle Wrapper and add the folder to git +* Gradle build +* implement SpotBugs, PMD and JaCoCo +* implement RR diagrams +* Move Special Oracle Test resources into the correct package +* Implement a basic Gradle/Maven compatibility workaround for the Special Oracle Test +* Fix the Gradle Wrapper and add the folder to git [6933d](https://github.com/JSQLParser/JSqlParser/commit/6933d86e0fa2d48) manticore-projects *2021-08-02 20:18:48* @@ -3149,17 +4021,17 @@ Changelog of JSqlParser. **Allowes JdbcParameter or JdbcNamedParameter for MySQL FullTextSearch (#1278)** -* Fixes issue #1223 +* Fixes issue #1223 [e6c91](https://github.com/JSQLParser/JSqlParser/commit/e6c91b6a813a1e0) manticore-projects *2021-07-26 21:06:38* **Fixes #1267 Cast into RowConstructor (#1274)** -* Fixes #1267 Cast into RowConstructor -* Improve Test Coverage -* Improve Test Coverage -* Improve Test Coverage -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Fixes #1267 Cast into RowConstructor +* Improve Test Coverage +* Improve Test Coverage +* Improve Test Coverage +* Co-authored-by: Tobias <t.warneke@gmx.net> [c89cf](https://github.com/JSQLParser/JSqlParser/commit/c89cf21641d672b) manticore-projects *2021-07-26 21:02:19* @@ -3170,20 +4042,20 @@ Changelog of JSqlParser. **Separate MySQL Special String Functions accepting Named Argument Separation as this could collide with ComplexExpressionList when InExpression is involved (#1285)** -* Fixes #1284 -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Fixes #1284 +* Co-authored-by: Tobias <t.warneke@gmx.net> [c074a](https://github.com/JSQLParser/JSqlParser/commit/c074a21ad70dd0e) manticore-projects *2021-07-26 20:34:06* **Implements Oracle RENAME oldTable TO newTable Statement (#1286)** -* Implements Oracle RENAME oldTable TO newTable Statement -* Fixes #1253 -* Implement MariaDB specific syntax -* Remove redundant License Headers -* Use LinkedHashMap to preserve the order of the Entries -* Increase Test Coverage -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Implements Oracle RENAME oldTable TO newTable Statement +* Fixes #1253 +* Implement MariaDB specific syntax +* Remove redundant License Headers +* Use LinkedHashMap to preserve the order of the Entries +* Increase Test Coverage +* Co-authored-by: Tobias <t.warneke@gmx.net> [f7f7b](https://github.com/JSQLParser/JSqlParser/commit/f7f7bcd65be8f4c) manticore-projects *2021-07-26 20:26:47* @@ -3204,17 +4076,17 @@ Changelog of JSqlParser. **Json functions (#1263)** -* Implement Json Aggregate Functions JSON_OBJECTAGG() and JSON_ARRAYAGG() -* fix the returned type -* Implement JSON_OBJECT and JSON_ARRAY -* Solves #1260 and dbeaver/dbeaver/#13141 -* Better workaround for NULL, NULL NULL ON NULL -* Remove the workaround for NULL ON NULL (without expression) -* Implement "PMD.MissingBreakInSwitch" in order to appease Codacy -* Improve Test Coverage -* Improve Test Coverage -* KEYs can be SQL Value Expressions -* Add another testcase +* Implement Json Aggregate Functions JSON_OBJECTAGG() and JSON_ARRAYAGG() +* fix the returned type +* Implement JSON_OBJECT and JSON_ARRAY +* Solves #1260 and dbeaver/dbeaver/#13141 +* Better workaround for NULL, NULL NULL ON NULL +* Remove the workaround for NULL ON NULL (without expression) +* Implement "PMD.MissingBreakInSwitch" in order to appease Codacy +* Improve Test Coverage +* Improve Test Coverage +* KEYs can be SQL Value Expressions +* Add another testcase [59bf0](https://github.com/JSQLParser/JSqlParser/commit/59bf07f5425ecb4) manticore-projects *2021-07-16 21:24:44* @@ -3225,20 +4097,20 @@ Changelog of JSqlParser. **Active JJDoc and let it create the Grammar BNF documentation (#1256)** -* Clean-up the Site generation +* Clean-up the Site generation [1ecff](https://github.com/JSQLParser/JSqlParser/commit/1ecffd2c1a6e8d8) manticore-projects *2021-07-16 20:38:05* **Bump commons-io from 2.6 to 2.7 (#1265)** -* Bumps commons-io from 2.6 to 2.7. -* --- -* updated-dependencies: -* - dependency-name: commons-io:commons-io -* dependency-type: direct:development -* ... -* Signed-off-by: dependabot[bot] <support@github.com> -* Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +* Bumps commons-io from 2.6 to 2.7. +* --- +* updated-dependencies: +* - dependency-name: commons-io:commons-io +* dependency-type: direct:development +* ... +* Signed-off-by: dependabot[bot] <support@github.com> +* Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [09898](https://github.com/JSQLParser/JSqlParser/commit/09898107fe6d001) dependabot[bot] *2021-07-14 05:27:57* @@ -3249,17 +4121,17 @@ Changelog of JSqlParser. **Implement DB2 Special Register Date Time CURRENT DATE and CURRENT TIME (#1252)** -* Implement DB2 Special Register Date Time CURRENT DATE and CURRENT TIME -* Fixes issue #1249 -* (Although there are more Special Registers which are not supported yet.) -* Make the spaces mandatory -* Add 2 more tests +* Implement DB2 Special Register Date Time CURRENT DATE and CURRENT TIME +* Fixes issue #1249 +* (Although there are more Special Registers which are not supported yet.) +* Make the spaces mandatory +* Add 2 more tests [05157](https://github.com/JSQLParser/JSqlParser/commit/05157a841897033) manticore-projects *2021-07-13 05:32:38* **Rename the PMD ruleset configuration file hoping for automatic synchronization with Codacy (#1251)** -* Solves Issue #1220 +* Solves Issue #1220 [930b7](https://github.com/JSQLParser/JSqlParser/commit/930b7a561876b7e) manticore-projects *2021-07-13 05:28:43* @@ -3310,8 +4182,8 @@ Changelog of JSqlParser. **Savepoint rollback (#1236)** -* Implement SAVEPOINT and ROLLBACK statements, fixes issue #1235 -* Activate a test which is supported now. +* Implement SAVEPOINT and ROLLBACK statements, fixes issue #1235 +* Activate a test which is supported now. [2cae6](https://github.com/JSQLParser/JSqlParser/commit/2cae62dbf74a744) manticore-projects *2021-06-30 19:57:56* @@ -3337,14 +4209,14 @@ Changelog of JSqlParser. **RESET statement, SET PostgreSQL compatibility (#1104)** -* Support -* RESET statement (https://www.postgresql.org/docs/current/sql-reset.html) -* SET [LOCAL|SESSION] (https://www.postgresql.org/docs/current/sql-set.html) -* SET search_path=my_schema, public ( https://www.postgresql.org/docs/current/sql-set.html -* value New value of parameter. Values can be specified as string constants, identifiers, numbers, or comma-separated lists of these) -* Update ResetStatementTest.java -* remove Tim Zone token -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Support +* RESET statement (https://www.postgresql.org/docs/current/sql-reset.html) +* SET [LOCAL|SESSION] (https://www.postgresql.org/docs/current/sql-set.html) +* SET search_path=my_schema, public ( https://www.postgresql.org/docs/current/sql-set.html +* value New value of parameter. Values can be specified as string constants, identifiers, numbers, or comma-separated lists of these) +* Update ResetStatementTest.java +* remove Tim Zone token +* Co-authored-by: Tobias <t.warneke@gmx.net> [13503](https://github.com/JSQLParser/JSqlParser/commit/13503edff30f06d) Роман Зотов *2021-06-26 22:44:33* @@ -3355,9 +4227,9 @@ Changelog of JSqlParser. **Implement Oracle Alter Session Statements (#1234)** -* Implement Oracle Alter Session Statements according to https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_2012.htm -* Implement PMD Rule "SwitchStmtsShouldHaveDefault" -* Reorganize Test Case imports +* Implement Oracle Alter Session Statements according to https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_2012.htm +* Implement PMD Rule "SwitchStmtsShouldHaveDefault" +* Reorganize Test Case imports [3a46a](https://github.com/JSQLParser/JSqlParser/commit/3a46a29d6936611) manticore-projects *2021-06-26 22:38:19* @@ -3368,16 +4240,16 @@ Changelog of JSqlParser. **Support DELETE FROM T1 USING T2 WHERE ... (#1228)** -* Co-authored-by: Francois Secherre <secherre.nospam@gmail.com> +* Co-authored-by: Francois Secherre <secherre.nospam@gmail.com> [96cd4](https://github.com/JSQLParser/JSqlParser/commit/96cd483ab85783d) francois-secherre *2021-06-16 05:27:14* **Row access support (#1181)** -* Row acess support -* Remove IN Left Expression List, replaced by RowConstructor Expression -* Remove IN Left Expression List, replaced by RowConstructor Expression -* Formatting +* Row acess support +* Remove IN Left Expression List, replaced by RowConstructor Expression +* Remove IN Left Expression List, replaced by RowConstructor Expression +* Formatting [27e6a](https://github.com/JSQLParser/JSqlParser/commit/27e6a9f0e07320e) Роман Зотов *2021-06-16 05:15:47* @@ -3388,67 +4260,67 @@ Changelog of JSqlParser. **Delete queries without from, with a schema identifier fails (#1224)** -* Delete queries without from, with a schema identifier fails -* Better tests -* Fix style issue -* Deparse should match for DELETE WITHOUT FROM queries -* Co-authored-by: François Sécherre <francois.secherre@ouicar.fr> +* Delete queries without from, with a schema identifier fails +* Better tests +* Fix style issue +* Deparse should match for DELETE WITHOUT FROM queries +* Co-authored-by: François Sécherre <francois.secherre@ouicar.fr> [d70e1](https://github.com/JSQLParser/JSqlParser/commit/d70e151c0f22f22) François Sécherre *2021-06-14 05:15:15* **Create temporary table t(c1, c2) as select ... (#1225)** -* Co-authored-by: Francois Secherre <secherre.nospam@gmail.com> +* Co-authored-by: Francois Secherre <secherre.nospam@gmail.com> [b62f1](https://github.com/JSQLParser/JSqlParser/commit/b62f19feb3b497c) francois-secherre *2021-06-14 05:13:13* **Nested with items (#1221)** -* Nested WithItems, fixes issue #1186 -* Remove redundant Test -* Avoid altering the nb-configuration -* Mention Nested WITH CTEs in the readme -* Eliminate dead/unused MultiExpression Code +* Nested WithItems, fixes issue #1186 +* Remove redundant Test +* Avoid altering the nb-configuration +* Mention Nested WITH CTEs in the readme +* Eliminate dead/unused MultiExpression Code [8eb3d](https://github.com/JSQLParser/JSqlParser/commit/8eb3d9a586a182e) manticore-projects *2021-06-10 05:50:08* **Implement GROUP BY () without columns (#1218)** -* Implement GROUP BY () without columns -* Migrate GroupByElement to ExpressionList -* Also solves issue #1210 automatically -* Solves issue #1168, add a test for it. +* Implement GROUP BY () without columns +* Migrate GroupByElement to ExpressionList +* Also solves issue #1210 automatically +* Solves issue #1168, add a test for it. [999db](https://github.com/JSQLParser/JSqlParser/commit/999db01658c30f9) manticore-projects *2021-06-03 05:55:35* **TSQL Compliant NEXT VALUE FOR sequence_id (but keeping the spurious NEXTVAL FOR expression) (#1216)** -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Co-authored-by: Tobias <t.warneke@gmx.net> [7c212](https://github.com/JSQLParser/JSqlParser/commit/7c21242e893abcd) manticore-projects *2021-06-02 12:35:03* **Pmd clean up (#1215)** -* Add PMD Annotations in order to avoid useless exceptions for the Deparsers -* Add Eclipse Formatter configuration -* Fix typo -* Replace Comments on empty methods with Class wide PMD Annotation -* Do not enforce checkstyle formatting +* Add PMD Annotations in order to avoid useless exceptions for the Deparsers +* Add Eclipse Formatter configuration +* Fix typo +* Replace Comments on empty methods with Class wide PMD Annotation +* Do not enforce checkstyle formatting [53764](https://github.com/JSQLParser/JSqlParser/commit/537649bf28f641b) manticore-projects *2021-06-02 12:19:32* **Add support for boolean 'XOR' operator (#1193)** -* Add support for boolean 'XOR' operator -* XorExpression added to the ReflectionModelTest -* XorExpression case added to the SelectTest -* XorExpression cases added for Validation -* Additional tests added for code coverage. -* Code style fixed. -* Separate test case for XOR added. -* Imports explicitly added to avoid namespace pollution. -* Additional tests cases for precedence and associativity. -* Co-authored-by: Szabó Miklós <miklos.szabo@arh.hu> +* Add support for boolean 'XOR' operator +* XorExpression added to the ReflectionModelTest +* XorExpression case added to the SelectTest +* XorExpression cases added for Validation +* Additional tests added for code coverage. +* Code style fixed. +* Separate test case for XOR added. +* Imports explicitly added to avoid namespace pollution. +* Additional tests cases for precedence and associativity. +* Co-authored-by: Szabó Miklós <miklos.szabo@arh.hu> [c7832](https://github.com/JSQLParser/JSqlParser/commit/c7832402dded4c0) Adaptive Recognition *2021-06-02 12:10:56* @@ -3484,20 +4356,20 @@ Changelog of JSqlParser. **Allow Complex Parsing of Functions (#1200)** -* Allow Complex Parsing of Functions -* Fixes issues #1190 #1103 -* Apply Complex Parsing to PrimaryExpression() -* Fixes issue #1194 -* Increase Test Timeout to 2 seconds for slow CI Servers. -* Appease Codazy +* Allow Complex Parsing of Functions +* Fixes issues #1190 #1103 +* Apply Complex Parsing to PrimaryExpression() +* Fixes issue #1194 +* Increase Test Timeout to 2 seconds for slow CI Servers. +* Appease Codazy [3a5da](https://github.com/JSQLParser/JSqlParser/commit/3a5da445ea9cb85) manticore-projects *2021-05-26 20:35:10* **Add support for AT TIME ZONE expressions (#1196)** -* Add support for AT TIME ZONE expressions -* adding tests -* Fixing imports +* Add support for AT TIME ZONE expressions +* adding tests +* Fixing imports [a5204](https://github.com/JSQLParser/JSqlParser/commit/a5204f63ccb1ea8) Tomer Shay (Shimshi) *2021-05-25 23:03:31* @@ -3523,9 +4395,9 @@ Changelog of JSqlParser. **Fix Nested CASE WHEN performance, fixes issue #1162 (#1208)** -* Fix Nested CASE WHEN performance, fixes issue #1162 -* Apease Codazy -* Apease Codazy +* Fix Nested CASE WHEN performance, fixes issue #1162 +* Apease Codazy +* Apease Codazy [42610](https://github.com/JSQLParser/JSqlParser/commit/426102e4cf272ca) manticore-projects *2021-05-25 19:26:30* @@ -3551,70 +4423,70 @@ Changelog of JSqlParser. **supporting/fixing unique inside sql function such as count eg - SELECT count(UNIQUE col2) FROM mytable (#1184)** -* Co-authored-by: Adhikesavan <radhikesavan@paypal.com> +* Co-authored-by: Adhikesavan <radhikesavan@paypal.com> [f18e9](https://github.com/JSQLParser/JSqlParser/commit/f18e92eaf4b3bc6) RajaSudharsan Adhikesavan *2021-05-01 19:28:05* **Oracle compliant ALTER TABLE ADD/MODIFY deparser (#1163)** -* javadoc-fixes -* fix check-style error : assignment to parameter not allowed -* import for javadoc reference -* javadoc - add description to parameter "fqn" (fix warning) -* remove doclint=none, but exclude package with exclude package with -* generated sources (javacc/jjtree) from javadoc -* Implement Oracle Hints for INSERT, UPDATE, MERGE, DELETE -* Correct CreateIndex TailOptions -* Add a Test Case for CreateIndex TailOptions -* Add WHERE expression to MergeInsert -* Add test case for MergeInsert WHERE expression -* Fix Issue #1156: ALTER TABLE ADD FOREIGN KEY with schema reference -* Add a specific test case -* Fix Issue #1157: Oracle does not accept COLUMN keyword in ALTER TABLE ADD/MODIFY -* Correct the test cases accepting a non existing COLUMN keyword -* Add a specific test cases -* Fix Issue #1164 UNIQUE after PRIMARY KEY -* Add test case for UNIQUE after PRIMARY KEY -* Switch of warnings for un-fixble method namings -* Switch of warnings for un-fixble method namings -* Activate PMD and define our own ruleset -* Execute PMD before building/testing in order to fail early -* Fix 63 PMD warnings -* Activate rule "PMD.CyclomaticComplexity" in order to simulate the Codazy checks -* Apply @SuppressWarnings({"PMD.CyclomaticComplexity"}) where this rule throws an unavoidable warning (especially for toString() and deparse()) -* Activate rule , "PMD.ExcessiveMethodLength" in order to simulate the Codazy checks -* Apply @SuppressWarnings({"PMD.ExcessiveMethodLength"}) where this rule throws an unavoidable warning (especially for toString() and deparse()) -* Refactor an ENUM name -* Refactor an ENUM name and reflect this also in the JavaCC Parser definition file -* Co-authored-by: gitmotte <www@synbee.at> +* javadoc-fixes +* fix check-style error : assignment to parameter not allowed +* import for javadoc reference +* javadoc - add description to parameter "fqn" (fix warning) +* remove doclint=none, but exclude package with exclude package with +* generated sources (javacc/jjtree) from javadoc +* Implement Oracle Hints for INSERT, UPDATE, MERGE, DELETE +* Correct CreateIndex TailOptions +* Add a Test Case for CreateIndex TailOptions +* Add WHERE expression to MergeInsert +* Add test case for MergeInsert WHERE expression +* Fix Issue #1156: ALTER TABLE ADD FOREIGN KEY with schema reference +* Add a specific test case +* Fix Issue #1157: Oracle does not accept COLUMN keyword in ALTER TABLE ADD/MODIFY +* Correct the test cases accepting a non existing COLUMN keyword +* Add a specific test cases +* Fix Issue #1164 UNIQUE after PRIMARY KEY +* Add test case for UNIQUE after PRIMARY KEY +* Switch of warnings for un-fixble method namings +* Switch of warnings for un-fixble method namings +* Activate PMD and define our own ruleset +* Execute PMD before building/testing in order to fail early +* Fix 63 PMD warnings +* Activate rule "PMD.CyclomaticComplexity" in order to simulate the Codazy checks +* Apply @SuppressWarnings({"PMD.CyclomaticComplexity"}) where this rule throws an unavoidable warning (especially for toString() and deparse()) +* Activate rule , "PMD.ExcessiveMethodLength" in order to simulate the Codazy checks +* Apply @SuppressWarnings({"PMD.ExcessiveMethodLength"}) where this rule throws an unavoidable warning (especially for toString() and deparse()) +* Refactor an ENUM name +* Refactor an ENUM name and reflect this also in the JavaCC Parser definition file +* Co-authored-by: gitmotte <www@synbee.at> [83837](https://github.com/JSQLParser/JSqlParser/commit/838379f21be0d32) manticore-projects *2021-04-21 07:55:17* **Pmd (#1165)** -* Implement Oracle Hints for INSERT, UPDATE, MERGE, DELETE -* Correct CreateIndex TailOptions -* Add a Test Case for CreateIndex TailOptions -* Add WHERE expression to MergeInsert -* Add test case for MergeInsert WHERE expression -* Fix Issue #1156: ALTER TABLE ADD FOREIGN KEY with schema reference -* Add a specific test case -* Fix Issue #1157: Oracle does not accept COLUMN keyword in ALTER TABLE ADD/MODIFY -* Correct the test cases accepting a non existing COLUMN keyword -* Add a specific test cases -* Fix Issue #1164 UNIQUE after PRIMARY KEY -* Add test case for UNIQUE after PRIMARY KEY -* Switch of warnings for un-fixble method namings -* Switch of warnings for un-fixble method namings -* Activate PMD and define our own ruleset -* Execute PMD before building/testing in order to fail early -* Fix 63 PMD warnings -* Activate rule "PMD.CyclomaticComplexity" in order to simulate the Codazy checks -* Apply @SuppressWarnings({"PMD.CyclomaticComplexity"}) where this rule throws an unavoidable warning (especially for toString() and deparse()) -* Activate rule , "PMD.ExcessiveMethodLength" in order to simulate the Codazy checks -* Apply @SuppressWarnings({"PMD.ExcessiveMethodLength"}) where this rule throws an unavoidable warning (especially for toString() and deparse()) -* Refactor an ENUM name -* Refactor an ENUM name and reflect this also in the JavaCC Parser definition file +* Implement Oracle Hints for INSERT, UPDATE, MERGE, DELETE +* Correct CreateIndex TailOptions +* Add a Test Case for CreateIndex TailOptions +* Add WHERE expression to MergeInsert +* Add test case for MergeInsert WHERE expression +* Fix Issue #1156: ALTER TABLE ADD FOREIGN KEY with schema reference +* Add a specific test case +* Fix Issue #1157: Oracle does not accept COLUMN keyword in ALTER TABLE ADD/MODIFY +* Correct the test cases accepting a non existing COLUMN keyword +* Add a specific test cases +* Fix Issue #1164 UNIQUE after PRIMARY KEY +* Add test case for UNIQUE after PRIMARY KEY +* Switch of warnings for un-fixble method namings +* Switch of warnings for un-fixble method namings +* Activate PMD and define our own ruleset +* Execute PMD before building/testing in order to fail early +* Fix 63 PMD warnings +* Activate rule "PMD.CyclomaticComplexity" in order to simulate the Codazy checks +* Apply @SuppressWarnings({"PMD.CyclomaticComplexity"}) where this rule throws an unavoidable warning (especially for toString() and deparse()) +* Activate rule , "PMD.ExcessiveMethodLength" in order to simulate the Codazy checks +* Apply @SuppressWarnings({"PMD.ExcessiveMethodLength"}) where this rule throws an unavoidable warning (especially for toString() and deparse()) +* Refactor an ENUM name +* Refactor an ENUM name and reflect this also in the JavaCC Parser definition file [08cfd](https://github.com/JSQLParser/JSqlParser/commit/08cfd29459044c6) manticore-projects *2021-04-20 08:01:48* @@ -3635,13 +4507,13 @@ Changelog of JSqlParser. **Assorted fixes to the Java CC Parser definition (#1153)** -* Implement Oracle Hints for INSERT, UPDATE, MERGE, DELETE -* Correct CreateIndex TailOptions -* Add a Test Case for CreateIndex TailOptions -* Add WHERE expression to MergeInsert -* Add test case for MergeInsert WHERE expression -* Fix Issue #1156: ALTER TABLE ADD FOREIGN KEY with schema reference -* Add a specific test case +* Implement Oracle Hints for INSERT, UPDATE, MERGE, DELETE +* Correct CreateIndex TailOptions +* Add a Test Case for CreateIndex TailOptions +* Add WHERE expression to MergeInsert +* Add test case for MergeInsert WHERE expression +* Fix Issue #1156: ALTER TABLE ADD FOREIGN KEY with schema reference +* Add a specific test case [5ee6e](https://github.com/JSQLParser/JSqlParser/commit/5ee6ec9dd7a66bf) manticore-projects *2021-04-16 22:51:27* @@ -3687,8 +4559,8 @@ Changelog of JSqlParser. **Add support for union without brackets and with limit (#1132)** -* add support for union without brackets and with limit -* Fixing the last commit. +* add support for union without brackets and with limit +* Fixing the last commit. [56c2d](https://github.com/JSQLParser/JSqlParser/commit/56c2dfe332b5ee8) Tomer Shay (Shimshi) *2021-03-14 20:09:12* @@ -3719,7 +4591,7 @@ Changelog of JSqlParser. **bug fix (#769)** -* Co-authored-by: Kunal Jha <kjha@zalando-11116.corp.ad.zalando.net> +* Co-authored-by: Kunal Jha <kjha@zalando-11116.corp.ad.zalando.net> [7234d](https://github.com/JSQLParser/JSqlParser/commit/7234de1d65ccf1b) Kunal jha *2021-02-05 17:46:15* @@ -3730,12 +4602,12 @@ Changelog of JSqlParser. **Array contructor support (#1105)** -* Array contructor support array[[1, 2], [id1, id2]] -* ARRAYLITERAL->ARRAY_LITERAL -* Fix empty array -* Support ARRAY as DEFAULT value in CREATE TABLE -* https://github.com/JSQLParser/JSqlParser/issues/970#issue-594819872 -* fix empty Array +* Array contructor support array[[1, 2], [id1, id2]] +* ARRAYLITERAL->ARRAY_LITERAL +* Fix empty array +* Support ARRAY as DEFAULT value in CREATE TABLE +* https://github.com/JSQLParser/JSqlParser/issues/970#issue-594819872 +* fix empty Array [43c28](https://github.com/JSQLParser/JSqlParser/commit/43c282deaa05555) Роман Зотов *2021-02-04 19:28:49* @@ -3746,13 +4618,13 @@ Changelog of JSqlParser. **Partial support construct tuple as simple expression (#1107)** -* SELECT (1,2) +* SELECT (1,2) [2065f](https://github.com/JSQLParser/JSqlParser/commit/2065fedb5b4c318) Роман Зотов *2021-01-31 22:58:08* **support create table parameters without columns, parameter values any names (#1106)** -* CREATE TEMPORARY TABLE t1 WITH (APPENDONLY=true,ORIENTATION=column,COMPRESSTYPE=zlib,OIDS=FALSE) ON COMMIT DROP AS SELECT column FROM t2 +* CREATE TEMPORARY TABLE t1 WITH (APPENDONLY=true,ORIENTATION=column,COMPRESSTYPE=zlib,OIDS=FALSE) ON COMMIT DROP AS SELECT column FROM t2 [f2e74](https://github.com/JSQLParser/JSqlParser/commit/f2e74f15cd63d87) Роман Зотов *2021-01-31 22:53:09* @@ -3938,8 +4810,8 @@ Changelog of JSqlParser. **support IN with value (#1065)** -* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> -* Co-authored-by: Tobias <t.warneke@gmx.net> +* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> +* Co-authored-by: Tobias <t.warneke@gmx.net> [8c7ee](https://github.com/JSQLParser/JSqlParser/commit/8c7ee289e78d07d) Jan Monterrubio *2020-11-22 19:29:27* @@ -3960,11 +4832,11 @@ Changelog of JSqlParser. **Support CreateSynonym statement (#1064)** -* visual -* add synonym support -* add tests -* exclude keyword -* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> +* visual +* add synonym support +* add tests +* exclude keyword +* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> [17e26](https://github.com/JSQLParser/JSqlParser/commit/17e2633e46778c6) Jan Monterrubio *2020-11-06 21:45:14* @@ -3975,352 +4847,352 @@ Changelog of JSqlParser. **Validation visitor framework (#1045)** -* * add with prefix for fluent setters. -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* add getters -* * add with prefix for fluent setters. (revert to chaining setters, do -* not break current api) -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* * add with prefix for fluent setters. (revert to chaining setters, do -* not break current api) -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* remove create() methods - they do not add enough value to be justified -* * use new methods within testcases -* add some constructors -* fix and add "with" / "add" methods -* * use new methods within testcases -* * use new methods within testcases -* add some constructors -* * renamed constant -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* * use new methods within testcases -* add some with-methods -* add getter/setter named after the field without abbrivation -* * use new methods within testcases -* remove empty implicit constructor -* return the deparsed Statement - object -* compare object tree -* compare object tree -* * fix ObjectTreeToStringStyle -* compare object tree -* remove casts not needed -* * use new methods within testcases -* add some "set" "with" "add" methods missing -* * use new methods within testcases -* add empty constructors and override with-/add-methods returning concrete -* type -* * add ReflectionModelTest -* * use new methods within testcases -* fix checkstyle errors -* license header -* remove test-classes from ReflectionModelTest -* remove visitoradapter-classes from ReflectionModelTest -* * add SelectDeParser(StringBuilder) -* remove overriding setters/getters of buffer -* #1007 -* push to synbee-contrib -* org.synbee.commons.contrib:jsqlparser:3.2-0.0.6-SNAPSHOT -* add ValidationUtil for simple validation of one or more statements -* remove overrides of -* getCause -* printStackTrace variants -* why add an additional cause ? -* set cause.getMessage() the message within constructor -* JSQLParserException(Throwable cause), othewise cause.toString() will be -* set as default. -* add ValidationVisitor showcase -* https://github.com/JSQLParser/JSqlParser/issues/1005 -* add ValidationUtil for simple validation of one or more statements -* remove overrides of -* getCause -* printStackTrace variants -* why add an additional cause ? -* set cause.getMessage() the message within constructor -* JSQLParserException(Throwable cause), othewise cause.toString() will be -* set as default. -* visit(ShowTablesStatement) -* copyright/license -* add stubs (use deparsers as template) -* Merge branch 'master.validate' of -* https://github.com/gitmotte/JSqlParser.git into master.validate -* add ValidationVisitor showcase -* https://github.com/JSQLParser/JSqlParser/issues/1005 -* add ValidationUtil for simple validation of one or more statements -* remove overrides of -* getCause -* printStackTrace variants -* why add an additional cause ? -* set cause.getMessage() the message within constructor -* JSQLParserException(Throwable cause), othewise cause.toString() will be -* set as default. -* visit(ShowTablesStatement) -* add stubs (use deparsers as template) -* Merge branch 'master.validate' of -* https://github.com/gitmotte/JSqlParser.git into master.validate -* add tests for ValidationUtil -* + implements OrderByVisitor -* split Expressionvalidator which implements both ItemsListVisitor and -* Expressionvisitor into Expressionvalidator and ItemListValidator -* Merge branch 'github.validate' -* implement upsertvalidator -* add copyright -* validate through given ValidationCapability's -* * switch to new method forced by -* ValidationCapability.validate(ValidationContext context, -* Consumer<String> errorMessageConsumer); -* add AllowedTypesValidation -* add FeatureConfiguration -* use FeatureConfiguration within parser -* repair pom.xml -* repair pom.xml -* repair pom.xml -* repair pom.xml -* * make FeatureConfiguration not a singleton any more -* CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> -* add FeaturesAllowed for testing against features allowed -* implement some Validators -* basic implementation of DatabaseMetaDataValidation / -* JdbcDatabaseMetaDataCapability -* moving classes to sub-packages -* * moving classes to sub-packages -* fixing some bugs -* repair pom.xml -* add and fix validations -* add javadoc -* * force definition of ```public String getMessage(Feature feature)``` -* in FeatureSetValidation -* allow all objects as feature-value - this may be needed by the parser, -* if a none-boolean configuration is needed -* impl. -* SelectValidator.visit(PlainSelect) -* OrderByValidator -* add Version-enums -* impl. -* InsertValidator -* multiple implementations of visit(SubSelect) -> forward to -* SelectValidator -* add some known features to SqlServerVersion -* refactoring enum-name should be upper case -* add ansi sql enum -* refactoring enum-name should be upper case -* implement limitvalidator -* + validateOffset -* + validateFetch -* + validate Pivot, UnPivot, PivotXml -* + implement DropValidator -* change testcase to image a more probably usecase -* * add javadoc and -* predefined sets for EXECUTE, ALTER, DROP -* allow to combine FeatureSets -* * implement executevalidator -* implement ExpressionValidator -* implement GrantValidator -* javadoc and complete SELECT constant -* use utility methods from AbstractValidator -* more user friendly names -* javadoc -* add subtypes for ValidationException -* ValidationParseException -* DatabaseException -* UnexpectedValidationException -* and change Set<String> errors to Set<ValidationException> for collect. -* javadoc & rename exception -* rename method -* extract parsing task into package - private class for {@link -* ValidationUtil} to parse the statements -* within it's own {@link ValidationCapability} -* add null-check for parsedStatement -* bugfix - do not collect duplicates -* implement toString() for -* ValidationError -* ValidationException -* add simple caching -* + validateOptionalFromItem(s) -* * implement GroupByValidator -* implement merge-validator -* renaming ItemListValidator -> ItemsListValidator -* + validateOptionalItemsList -* + implement ReplaceValidator -* + use validateOptionalColumns, validateOptionalExpression where possible -* * remove validateOptionalColumns -> switch to -* validateOptionalExpressions -* move validateOptionalOrderByElements to AbstractValidator -* add validateOptional in AbstractValidator -* add validateOptionalList in AbstractValidator -* + SetStatementValidator -* + ValuesStatementValidator -* + UseStatementValidator -* * implement UpdateValidator -* * implement ShowStatementValidator/ShowColumnsStatementValidator -* * implement UpdateValidator -* * add Feature.jdbcParameter, Feature.jdbcNamedParameter, to all -* featuresets -* + Version.getFeaturesClone -* add javadoc to Version-enum-constructors -* + validateOptionalFeature -* * implement DeleteValidator -* ... -* fix typo -* small optimization -* * move method getFeaturesClone to FeatureSet -* implement join - validation -* add copy(), add(Collection), remove(*) methods to FeaturesAllowed -* * add join - features to sqlserver, h2 -* implementations -* bugfix - merging the errors -* copyright -* https://github.com/JSQLParser/JSqlParser/issues/1022 -* add more fine granular control for setOperations -* fix nullpointerexception -* add more fine granular control for comments -* add Features supported -* * add javadoc -* add features to *Version-files -* extract methods isNotEmpty -* check for isNotEmpty -* * add features to *Version-files -* always parse net.sf.jsqlparser.statement.Statements and validate the -* list of included net.sf.jsqlparser.statement.Statement's -* add known mariadb features -* new names-set for FeaturesAllowed -* new names-set for FeaturesAllowed -* new names-set for FeaturesAllowed -* add ature.withItem, Feature.withItemRecursive to H2 -* Feature.setOperation, Feature.setOperationUnion, -* Feature.setOperationIntersect, Feature.setOperationExcept, -* for MariaDb -* add features to SQLServer -* Merge branch 'master.orig' into github.validate -* @Override() -> @Override -* fix typing error "joinStaight" > joinStraight -* rename Feature "insertValues" -> "values" and use "insertValues" for -* INSERT INTO ... VALUES -* add javadoc -* add Feature.selectGroupByGroupingSets to PostgresqlVersion -* implement basic OracleVersion -* add Feature.mySql* - also supported by mariadb -* add some more finegraned control over "drop" Feature. -* drop, -* dropTable, -* dropIndex, -* dropView, -* dropSchema, -* dropSequence, -* dropIfExists, -* complete FeaturesAllowed groups INSERT/UPDATE/DELETE/MERGE/DML -* add link to documentation -* fix - duplicate use of feature "function" - the use of functions in -* statements and "createFunction" as a ddl statement -* TODO this feature seams very close to a jsqlparser-user usecase -* * implement MySqlVersion -* replace feature Feature.dropIfExists by features dropTableIfExists, -* dropIndexIfExists, dropViewIfExists, dropSchemaIfExists, -* dropSequenceIfExists -* add methods FeatureSet.getNotContained FeatureSet.retainAll -* remove HSQLDBVersion - do not support this variant -* remove HSQLDBVersion - do not support this variant -* add unit-test -* + add unittests for -* UpdateValidator -* DeleteValidator -* add stubs for all other Validator-classes -* + ModifyableFeatureSet -* add some utility-methods in ValidationTestAsserts -* complete unit-tests for InsertValidator -* remote Feature.insertReturningExpressionList for Oracle - -* returning_clause requires INTO clause (only PL/SQL) -* add some more select validation tests -* add DropValidatorTests -* add DropValidatorTests -* add CreateTableValidatorTests -* add CreateTableValidatorTests -* add ExpressionValidatorTests -* add OrderByValidatorTest -* use isNotEmpty -* implement GroupByValidatorTest -* implement CreateSequenceValidatorTest -* remove @Ignore - test is ok -* implement CreateIndexValidatorTest -* implement CreateViewValidatorTest -* enable validation of Feature.commentOnView (#1024 is merged already) -* change format of #toString() for better readability -* * implement MergeValidatorTest -* implement ReplaceValidatorTest -* implement StatementValidatorTest -* rename -* ValidationUtil -> Validation -* ValidatorUtil -> ValidationUtil -* add testcases for ValidationUtil -* add DatabaseMetaDataValidationTest -* checkstyle fix -* add copyright statement -* add unit-tests for show tables, show column, show statements -* * add ExecuteValidatorTest -* as there is a difference between execute <procedure> and execute -* [immediate] <dynamic sql> with USING expr, ... remove support for -* execute on MYSQL, MARIADB, ORACLE -* * add ExecuteValidatorTest for CALL fnName (mysql, mariadb, postgres) -* add upsertvalidatortest -* add GrantValidatorTest -* add AlterSequenceValidatorTest -* add AlterSequenceValidatorTest -* add AlterViewValidatorTest -* add AlterValidatorTest -* replace != null by isNotEmpty on collections -* fix formatting -* add validate commit -* add validate block -* add DeclareStatementValidatorTest -* let NamesLookup implement UnaryOperator<String> -* let NamesLookup implement UnaryOperator<String> -* add javadoc -* add more DatabaseMetaDataValidationTest's -* extract JdbcDatabaseMetaDataCapability.splitAndValidateMinMax -* add pivot/unpivot/pivotxml validation testcases -* add testcase for Feature.tableFunction -* add test for lateral joins and subjoins -* add testValidationRowMovementOption -* add values validator test -* move tests to LimitValidatorTest -* move tests to UseStatementValidatorTest -* add tests for SET - statements -* fix checkstyle error -* new serialVersionUID -* add validation for NamedObject not existing -* need table/view reference to validate column names -* fix typo -* fix errormessage (Arrays.toString(types)) -* add trigger, alias -* return null, instead of throwing exception, if not found -* extract NamesLookup to own file (jdk-bug enum inner classes) -* fix name-check AlterOperation.ALTER -* fix error message -* remove methods not needed (they only delegate to ValidationContext) -* add tests - validate metadata -* fix compile error -* fix columnExists check - depending on the statement the prefix is an -* alias, a table/view or it has no prefix (need to lookup within all -* related tables/views) -* fix javadoc warnings +* * add with prefix for fluent setters. +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* add getters +* * add with prefix for fluent setters. (revert to chaining setters, do +* not break current api) +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* * add with prefix for fluent setters. (revert to chaining setters, do +* not break current api) +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* remove create() methods - they do not add enough value to be justified +* * use new methods within testcases +* add some constructors +* fix and add "with" / "add" methods +* * use new methods within testcases +* * use new methods within testcases +* add some constructors +* * renamed constant +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* * use new methods within testcases +* add some with-methods +* add getter/setter named after the field without abbrivation +* * use new methods within testcases +* remove empty implicit constructor +* return the deparsed Statement - object +* compare object tree +* compare object tree +* * fix ObjectTreeToStringStyle +* compare object tree +* remove casts not needed +* * use new methods within testcases +* add some "set" "with" "add" methods missing +* * use new methods within testcases +* add empty constructors and override with-/add-methods returning concrete +* type +* * add ReflectionModelTest +* * use new methods within testcases +* fix checkstyle errors +* license header +* remove test-classes from ReflectionModelTest +* remove visitoradapter-classes from ReflectionModelTest +* * add SelectDeParser(StringBuilder) +* remove overriding setters/getters of buffer +* #1007 +* push to synbee-contrib +* org.synbee.commons.contrib:jsqlparser:3.2-0.0.6-SNAPSHOT +* add ValidationUtil for simple validation of one or more statements +* remove overrides of +* getCause +* printStackTrace variants +* why add an additional cause ? +* set cause.getMessage() the message within constructor +* JSQLParserException(Throwable cause), othewise cause.toString() will be +* set as default. +* add ValidationVisitor showcase +* https://github.com/JSQLParser/JSqlParser/issues/1005 +* add ValidationUtil for simple validation of one or more statements +* remove overrides of +* getCause +* printStackTrace variants +* why add an additional cause ? +* set cause.getMessage() the message within constructor +* JSQLParserException(Throwable cause), othewise cause.toString() will be +* set as default. +* visit(ShowTablesStatement) +* copyright/license +* add stubs (use deparsers as template) +* Merge branch 'master.validate' of +* https://github.com/gitmotte/JSqlParser.git into master.validate +* add ValidationVisitor showcase +* https://github.com/JSQLParser/JSqlParser/issues/1005 +* add ValidationUtil for simple validation of one or more statements +* remove overrides of +* getCause +* printStackTrace variants +* why add an additional cause ? +* set cause.getMessage() the message within constructor +* JSQLParserException(Throwable cause), othewise cause.toString() will be +* set as default. +* visit(ShowTablesStatement) +* add stubs (use deparsers as template) +* Merge branch 'master.validate' of +* https://github.com/gitmotte/JSqlParser.git into master.validate +* add tests for ValidationUtil +* + implements OrderByVisitor +* split Expressionvalidator which implements both ItemsListVisitor and +* Expressionvisitor into Expressionvalidator and ItemListValidator +* Merge branch 'github.validate' +* implement upsertvalidator +* add copyright +* validate through given ValidationCapability's +* * switch to new method forced by +* ValidationCapability.validate(ValidationContext context, +* Consumer<String> errorMessageConsumer); +* add AllowedTypesValidation +* add FeatureConfiguration +* use FeatureConfiguration within parser +* repair pom.xml +* repair pom.xml +* repair pom.xml +* repair pom.xml +* * make FeatureConfiguration not a singleton any more +* CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> +* add FeaturesAllowed for testing against features allowed +* implement some Validators +* basic implementation of DatabaseMetaDataValidation / +* JdbcDatabaseMetaDataCapability +* moving classes to sub-packages +* * moving classes to sub-packages +* fixing some bugs +* repair pom.xml +* add and fix validations +* add javadoc +* * force definition of ```public String getMessage(Feature feature)``` +* in FeatureSetValidation +* allow all objects as feature-value - this may be needed by the parser, +* if a none-boolean configuration is needed +* impl. +* SelectValidator.visit(PlainSelect) +* OrderByValidator +* add Version-enums +* impl. +* InsertValidator +* multiple implementations of visit(SubSelect) -> forward to +* SelectValidator +* add some known features to SqlServerVersion +* refactoring enum-name should be upper case +* add ansi sql enum +* refactoring enum-name should be upper case +* implement limitvalidator +* + validateOffset +* + validateFetch +* + validate Pivot, UnPivot, PivotXml +* + implement DropValidator +* change testcase to image a more probably usecase +* * add javadoc and +* predefined sets for EXECUTE, ALTER, DROP +* allow to combine FeatureSets +* * implement executevalidator +* implement ExpressionValidator +* implement GrantValidator +* javadoc and complete SELECT constant +* use utility methods from AbstractValidator +* more user friendly names +* javadoc +* add subtypes for ValidationException +* ValidationParseException +* DatabaseException +* UnexpectedValidationException +* and change Set<String> errors to Set<ValidationException> for collect. +* javadoc & rename exception +* rename method +* extract parsing task into package - private class for {@link +* ValidationUtil} to parse the statements +* within it's own {@link ValidationCapability} +* add null-check for parsedStatement +* bugfix - do not collect duplicates +* implement toString() for +* ValidationError +* ValidationException +* add simple caching +* + validateOptionalFromItem(s) +* * implement GroupByValidator +* implement merge-validator +* renaming ItemListValidator -> ItemsListValidator +* + validateOptionalItemsList +* + implement ReplaceValidator +* + use validateOptionalColumns, validateOptionalExpression where possible +* * remove validateOptionalColumns -> switch to +* validateOptionalExpressions +* move validateOptionalOrderByElements to AbstractValidator +* add validateOptional in AbstractValidator +* add validateOptionalList in AbstractValidator +* + SetStatementValidator +* + ValuesStatementValidator +* + UseStatementValidator +* * implement UpdateValidator +* * implement ShowStatementValidator/ShowColumnsStatementValidator +* * implement UpdateValidator +* * add Feature.jdbcParameter, Feature.jdbcNamedParameter, to all +* featuresets +* + Version.getFeaturesClone +* add javadoc to Version-enum-constructors +* + validateOptionalFeature +* * implement DeleteValidator +* ... +* fix typo +* small optimization +* * move method getFeaturesClone to FeatureSet +* implement join - validation +* add copy(), add(Collection), remove(*) methods to FeaturesAllowed +* * add join - features to sqlserver, h2 +* implementations +* bugfix - merging the errors +* copyright +* https://github.com/JSQLParser/JSqlParser/issues/1022 +* add more fine granular control for setOperations +* fix nullpointerexception +* add more fine granular control for comments +* add Features supported +* * add javadoc +* add features to *Version-files +* extract methods isNotEmpty +* check for isNotEmpty +* * add features to *Version-files +* always parse net.sf.jsqlparser.statement.Statements and validate the +* list of included net.sf.jsqlparser.statement.Statement's +* add known mariadb features +* new names-set for FeaturesAllowed +* new names-set for FeaturesAllowed +* new names-set for FeaturesAllowed +* add ature.withItem, Feature.withItemRecursive to H2 +* Feature.setOperation, Feature.setOperationUnion, +* Feature.setOperationIntersect, Feature.setOperationExcept, +* for MariaDb +* add features to SQLServer +* Merge branch 'master.orig' into github.validate +* @Override() -> @Override +* fix typing error "joinStaight" > joinStraight +* rename Feature "insertValues" -> "values" and use "insertValues" for +* INSERT INTO ... VALUES +* add javadoc +* add Feature.selectGroupByGroupingSets to PostgresqlVersion +* implement basic OracleVersion +* add Feature.mySql* - also supported by mariadb +* add some more finegraned control over "drop" Feature. +* drop, +* dropTable, +* dropIndex, +* dropView, +* dropSchema, +* dropSequence, +* dropIfExists, +* complete FeaturesAllowed groups INSERT/UPDATE/DELETE/MERGE/DML +* add link to documentation +* fix - duplicate use of feature "function" - the use of functions in +* statements and "createFunction" as a ddl statement +* TODO this feature seams very close to a jsqlparser-user usecase +* * implement MySqlVersion +* replace feature Feature.dropIfExists by features dropTableIfExists, +* dropIndexIfExists, dropViewIfExists, dropSchemaIfExists, +* dropSequenceIfExists +* add methods FeatureSet.getNotContained FeatureSet.retainAll +* remove HSQLDBVersion - do not support this variant +* remove HSQLDBVersion - do not support this variant +* add unit-test +* + add unittests for +* UpdateValidator +* DeleteValidator +* add stubs for all other Validator-classes +* + ModifyableFeatureSet +* add some utility-methods in ValidationTestAsserts +* complete unit-tests for InsertValidator +* remote Feature.insertReturningExpressionList for Oracle - +* returning_clause requires INTO clause (only PL/SQL) +* add some more select validation tests +* add DropValidatorTests +* add DropValidatorTests +* add CreateTableValidatorTests +* add CreateTableValidatorTests +* add ExpressionValidatorTests +* add OrderByValidatorTest +* use isNotEmpty +* implement GroupByValidatorTest +* implement CreateSequenceValidatorTest +* remove @Ignore - test is ok +* implement CreateIndexValidatorTest +* implement CreateViewValidatorTest +* enable validation of Feature.commentOnView (#1024 is merged already) +* change format of #toString() for better readability +* * implement MergeValidatorTest +* implement ReplaceValidatorTest +* implement StatementValidatorTest +* rename +* ValidationUtil -> Validation +* ValidatorUtil -> ValidationUtil +* add testcases for ValidationUtil +* add DatabaseMetaDataValidationTest +* checkstyle fix +* add copyright statement +* add unit-tests for show tables, show column, show statements +* * add ExecuteValidatorTest +* as there is a difference between execute <procedure> and execute +* [immediate] <dynamic sql> with USING expr, ... remove support for +* execute on MYSQL, MARIADB, ORACLE +* * add ExecuteValidatorTest for CALL fnName (mysql, mariadb, postgres) +* add upsertvalidatortest +* add GrantValidatorTest +* add AlterSequenceValidatorTest +* add AlterSequenceValidatorTest +* add AlterViewValidatorTest +* add AlterValidatorTest +* replace != null by isNotEmpty on collections +* fix formatting +* add validate commit +* add validate block +* add DeclareStatementValidatorTest +* let NamesLookup implement UnaryOperator<String> +* let NamesLookup implement UnaryOperator<String> +* add javadoc +* add more DatabaseMetaDataValidationTest's +* extract JdbcDatabaseMetaDataCapability.splitAndValidateMinMax +* add pivot/unpivot/pivotxml validation testcases +* add testcase for Feature.tableFunction +* add test for lateral joins and subjoins +* add testValidationRowMovementOption +* add values validator test +* move tests to LimitValidatorTest +* move tests to UseStatementValidatorTest +* add tests for SET - statements +* fix checkstyle error +* new serialVersionUID +* add validation for NamedObject not existing +* need table/view reference to validate column names +* fix typo +* fix errormessage (Arrays.toString(types)) +* add trigger, alias +* return null, instead of throwing exception, if not found +* extract NamesLookup to own file (jdk-bug enum inner classes) +* fix name-check AlterOperation.ALTER +* fix error message +* remove methods not needed (they only delegate to ValidationContext) +* add tests - validate metadata +* fix compile error +* fix columnExists check - depending on the statement the prefix is an +* alias, a table/view or it has no prefix (need to lookup within all +* related tables/views) +* fix javadoc warnings [8c735](https://github.com/JSQLParser/JSqlParser/commit/8c735be5b179e51) gitmotte *2020-11-06 21:12:25* **Support Create table LIKE (#1066)** -* fixes #413 -* add coverage -* Co-authored-by: chyun <chyun_wu@163.com> +* fixes #413 +* add coverage +* Co-authored-by: chyun <chyun_wu@163.com> [ac746](https://github.com/JSQLParser/JSqlParser/commit/ac7462286ae15b9) Chyun *2020-11-06 21:05:09* @@ -4331,12 +5203,12 @@ Changelog of JSqlParser. **Bump junit from 4.12 to 4.13.1 (#1063)** -* Bumps [junit](https://github.com/junit-team/junit4) from 4.12 to 4.13.1. -* - [Release notes](https://github.com/junit-team/junit4/releases) -* - [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.12.md) -* - [Commits](https://github.com/junit-team/junit4/compare/r4.12...r4.13.1) -* Signed-off-by: dependabot[bot] <support@github.com> -* Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +* Bumps [junit](https://github.com/junit-team/junit4) from 4.12 to 4.13.1. +* - [Release notes](https://github.com/junit-team/junit4/releases) +* - [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.12.md) +* - [Commits](https://github.com/junit-team/junit4/compare/r4.12...r4.13.1) +* Signed-off-by: dependabot[bot] <support@github.com> +* Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [f9a11](https://github.com/JSQLParser/JSqlParser/commit/f9a115c582dd59b) dependabot[bot] *2020-10-13 12:26:17* @@ -4352,8 +5224,8 @@ Changelog of JSqlParser. **support FILTER not only for window function (#1046)** -* support FILTER not only for window function -* Fixed imports +* support FILTER not only for window function +* Fixed imports [f32fa](https://github.com/JSQLParser/JSqlParser/commit/f32fa6137d6161b) Роман Зотов *2020-10-05 19:45:36* @@ -4374,7 +5246,7 @@ Changelog of JSqlParser. **Retain original value in TimestampValue (#1057)** -* Co-authored-by: Enrico Olivelli <enrico.olivelli@diennea.com> +* Co-authored-by: Enrico Olivelli <enrico.olivelli@diennea.com> [622f9](https://github.com/JSQLParser/JSqlParser/commit/622f9aebb3ebce7) Enrico Olivelli *2020-10-04 19:51:01* @@ -4385,8 +5257,8 @@ Changelog of JSqlParser. **Addons/fixes for Fluent API (#1049)** -* fix unittests for setter/wither methods with primitive arguments -* add missing withAscDescPresent +* fix unittests for setter/wither methods with primitive arguments +* add missing withAscDescPresent [8165e](https://github.com/JSQLParser/JSqlParser/commit/8165e29cb081080) gitmotte *2020-10-04 19:20:43* @@ -4432,8 +5304,8 @@ Changelog of JSqlParser. **bugfix #720 #991: supporting SELECT "conditions" (#1032)** -* bugfix issue #1020: JSON type in MySQL not supported in v3.2 -* bugfix issue #720 #991: supporting SELECT "CONDITIONS" +* bugfix issue #1020: JSON type in MySQL not supported in v3.2 +* bugfix issue #720 #991: supporting SELECT "CONDITIONS" [9e26b](https://github.com/JSQLParser/JSqlParser/commit/9e26b76ddbc626f) suiwenbo *2020-08-25 22:29:41* @@ -4449,101 +5321,101 @@ Changelog of JSqlParser. **Fluent builder api #1004 (#1014)** -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* create(...) methods -* chaining - methods returning "this" -* overwrite chaining - methods of abstract parents/interfaces for -* returning concrete type -* add<Name> methods on collection-fields with varargs-parameter -* add public T get<Name>(Class<T>) - casting and returning an inner -* interface-type -* 1004 add chaining - methods returning "this" -* #1004 add chaining - methods returning "this" -* * add<Name> methods on collection-fields with varargs-parameter -* add<Name> methods on collection-fields with collection-parameter -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* * add chaining - methods returning "this" -* add<Name> methods on collection-fields with varargs-parameter -* add<Name> methods on collection-fields with collection-parameter -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* * add public T get<Name>(Class<T>) - casting and returning the concrete -* type -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* * add public T get<Name>(Class<T>) - casting and returning the concrete -* type (swap Class<? extends E> for Class<E>) -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* * overwrite chaining - methods of abstract parents/interfaces for -* returning concrete type -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* * add with prefix for fluent setters. -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* add getters -* * add with prefix for fluent setters. (revert to chaining setters, do -* not break current api) -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* * add with prefix for fluent setters. (revert to chaining setters, do -* not break current api) -* https://github.com/JSQLParser/JSqlParser/issues/1004 -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* remove create() methods - they do not add enough value to be justified -* * use new methods within testcases -* add some constructors -* fix and add "with" / "add" methods -* * use new methods within testcases -* * use new methods within testcases -* add some constructors -* * renamed constant -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* use new methods within testcases -* * use new methods within testcases -* add some with-methods -* add getter/setter named after the field without abbrivation -* * use new methods within testcases -* remove empty implicit constructor -* return the deparsed Statement - object -* compare object tree -* compare object tree -* * fix ObjectTreeToStringStyle -* compare object tree -* remove casts not needed -* * use new methods within testcases -* add some "set" "with" "add" methods missing -* * use new methods within testcases -* add empty constructors and override with-/add-methods returning concrete -* type -* * add ReflectionModelTest -* * use new methods within testcases -* fix checkstyle errors -* license header -* remove test-classes from ReflectionModelTest -* remove visitoradapter-classes from ReflectionModelTest -* remove duplicate import declaration (checkstyle error) -* * fix RandomUtils to support used java.sql.* types -* fix RandomUtils to support enums -* fix RandomUtils to map objects by its interfaces and super-classes -* filter method "setASTNode" - do not test setters (cannot randomly -* create a SimpleNode) -* add javadoc, stating that this is a marker interface -* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454761902 -* revert formatting change -* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454762463 -* change to EXEC_TYPE.EXECUTE just so the assertion didn't change -* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454763565 -* try to revert format changes -* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454800430 -* try to revert format changes -* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454800430 -* remove brackets on @Override() -> @Override -* add with-methods to new fields +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* create(...) methods +* chaining - methods returning "this" +* overwrite chaining - methods of abstract parents/interfaces for +* returning concrete type +* add<Name> methods on collection-fields with varargs-parameter +* add public T get<Name>(Class<T>) - casting and returning an inner +* interface-type +* 1004 add chaining - methods returning "this" +* #1004 add chaining - methods returning "this" +* * add<Name> methods on collection-fields with varargs-parameter +* add<Name> methods on collection-fields with collection-parameter +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* * add chaining - methods returning "this" +* add<Name> methods on collection-fields with varargs-parameter +* add<Name> methods on collection-fields with collection-parameter +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* * add public T get<Name>(Class<T>) - casting and returning the concrete +* type +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* * add public T get<Name>(Class<T>) - casting and returning the concrete +* type (swap Class<? extends E> for Class<E>) +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* * overwrite chaining - methods of abstract parents/interfaces for +* returning concrete type +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* * add with prefix for fluent setters. +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* add getters +* * add with prefix for fluent setters. (revert to chaining setters, do +* not break current api) +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* * add with prefix for fluent setters. (revert to chaining setters, do +* not break current api) +* https://github.com/JSQLParser/JSqlParser/issues/1004 +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* remove create() methods - they do not add enough value to be justified +* * use new methods within testcases +* add some constructors +* fix and add "with" / "add" methods +* * use new methods within testcases +* * use new methods within testcases +* add some constructors +* * renamed constant +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* use new methods within testcases +* * use new methods within testcases +* add some with-methods +* add getter/setter named after the field without abbrivation +* * use new methods within testcases +* remove empty implicit constructor +* return the deparsed Statement - object +* compare object tree +* compare object tree +* * fix ObjectTreeToStringStyle +* compare object tree +* remove casts not needed +* * use new methods within testcases +* add some "set" "with" "add" methods missing +* * use new methods within testcases +* add empty constructors and override with-/add-methods returning concrete +* type +* * add ReflectionModelTest +* * use new methods within testcases +* fix checkstyle errors +* license header +* remove test-classes from ReflectionModelTest +* remove visitoradapter-classes from ReflectionModelTest +* remove duplicate import declaration (checkstyle error) +* * fix RandomUtils to support used java.sql.* types +* fix RandomUtils to support enums +* fix RandomUtils to map objects by its interfaces and super-classes +* filter method "setASTNode" - do not test setters (cannot randomly +* create a SimpleNode) +* add javadoc, stating that this is a marker interface +* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454761902 +* revert formatting change +* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454762463 +* change to EXEC_TYPE.EXECUTE just so the assertion didn't change +* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454763565 +* try to revert format changes +* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454800430 +* try to revert format changes +* https://github.com/JSQLParser/JSqlParser/pull/1014#discussion_r454800430 +* remove brackets on @Override() -> @Override +* add with-methods to new fields [6cff1](https://github.com/JSQLParser/JSqlParser/commit/6cff161dacc1e6f) gitmotte *2020-08-23 20:07:53* @@ -4559,27 +5431,27 @@ Changelog of JSqlParser. **Support Foreign Key ON UPDATE CASCADE (#1025)** -* https://github.com/JSQLParser/JSqlParser/issues/985 -* add 2 unit-tests for given statements -* https://github.com/JSQLParser/JSqlParser/issues/985 -* fix formating (line width) -* https://github.com/JSQLParser/JSqlParser/issues/985 -* * fix nullpointerexceptions -* add more unittest-assertions -* https://github.com/JSQLParser/JSqlParser/issues/985 -* change order to match the same order as in ForeignKeyIndex -* byAction should not throw an exception (is used by deprecated -* string-setters) -* add unit-tests for ReferentialAction within AlterExpression -* fix toString (added bug on refactoring) -* javadoc -* test set from get on null-values too -* refactoring: add and use ReferentialAction() to evaluate enum -* https://github.com/JSQLParser/JSqlParser/issues/985 -* refactoring: fix parser that order of referential actions does not -* matter -* https://github.com/JSQLParser/JSqlParser/issues/985 -* add empty constructor +* https://github.com/JSQLParser/JSqlParser/issues/985 +* add 2 unit-tests for given statements +* https://github.com/JSQLParser/JSqlParser/issues/985 +* fix formating (line width) +* https://github.com/JSQLParser/JSqlParser/issues/985 +* * fix nullpointerexceptions +* add more unittest-assertions +* https://github.com/JSQLParser/JSqlParser/issues/985 +* change order to match the same order as in ForeignKeyIndex +* byAction should not throw an exception (is used by deprecated +* string-setters) +* add unit-tests for ReferentialAction within AlterExpression +* fix toString (added bug on refactoring) +* javadoc +* test set from get on null-values too +* refactoring: add and use ReferentialAction() to evaluate enum +* https://github.com/JSQLParser/JSqlParser/issues/985 +* refactoring: fix parser that order of referential actions does not +* matter +* https://github.com/JSQLParser/JSqlParser/issues/985 +* add empty constructor [1e88d](https://github.com/JSQLParser/JSqlParser/commit/1e88dd57eb48ebf) gitmotte *2020-08-09 21:01:34* @@ -4605,10 +5477,10 @@ Changelog of JSqlParser. **COMMENT ON VIEW (#1024)** -* * implement COMMENT ON VIEW -* testcase "testCommentOnView" -* https://github.com/JSQLParser/JSqlParser/issues/1023 -* add more asserts +* * implement COMMENT ON VIEW +* testcase "testCommentOnView" +* https://github.com/JSQLParser/JSqlParser/issues/1023 +* add more asserts [449f5](https://github.com/JSQLParser/JSqlParser/commit/449f55219fc39eb) gitmotte *2020-08-09 20:49:05* @@ -4664,18 +5536,18 @@ Changelog of JSqlParser. **Add show tables support (#1015)** -* visual -* implement show tables -* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> +* visual +* implement show tables +* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> [c0373](https://github.com/JSQLParser/JSqlParser/commit/c03733b3cfcb758) Jan Monterrubio *2020-07-11 16:43:42* **let all deparsers extend AbstractDeParser (#1007)** -* let all deparsers extend AbstractDeParser -* * add SelectDeParser(StringBuilder) -* remove overriding setters/getters of buffer -* #1007 +* let all deparsers extend AbstractDeParser +* * add SelectDeParser(StringBuilder) +* remove overriding setters/getters of buffer +* #1007 [2b790](https://github.com/JSQLParser/JSqlParser/commit/2b7909c3be31ca8) gitmotte *2020-07-11 16:40:11* @@ -4711,13 +5583,13 @@ Changelog of JSqlParser. **Support options for Explain (#996)** -* visual -* issue-995 -* support verbose -* postgres explain -* tests -* no text -* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> +* visual +* issue-995 +* support verbose +* postgres explain +* tests +* no text +* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> [13873](https://github.com/JSQLParser/JSqlParser/commit/1387354712285e4) Jan Monterrubio *2020-06-23 21:52:42* @@ -4728,12 +5600,12 @@ Changelog of JSqlParser. **Support multiple lists for an IN clause (#997)** -* visual -* wip -* cleanup n test -* polish -* lookahead -* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> +* visual +* wip +* cleanup n test +* polish +* lookahead +* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> [5de4a](https://github.com/JSQLParser/JSqlParser/commit/5de4ae597fbeda3) Jan Monterrubio *2020-06-20 22:10:38* @@ -4749,8 +5621,8 @@ Changelog of JSqlParser. **Support ALTER SEQUENCE (#980)** -* support alter sequence -* improve coverage +* support alter sequence +* improve coverage [d34c8](https://github.com/JSQLParser/JSqlParser/commit/d34c885ba5a8c93) Jan Monterrubio *2020-05-23 10:16:07* @@ -4796,21 +5668,21 @@ Changelog of JSqlParser. **Implement row movement clause for table creation (#974)** -* visual -* implement row movement -* support row + AS -* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> +* visual +* implement row movement +* support row + AS +* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> [79b5f](https://github.com/JSQLParser/JSqlParser/commit/79b5fe9c5681961) Jan Monterrubio *2020-04-28 07:02:51* **Support CREATE SEQUENCE (#977)** -* wip -* wip, some parsing -* support sequence -* implement feature -* delete issue tests -* compile it +* wip +* wip, some parsing +* support sequence +* implement feature +* delete issue tests +* compile it [a6a3c](https://github.com/JSQLParser/JSqlParser/commit/a6a3c616b8994f1) Jan Monterrubio *2020-04-28 07:01:10* @@ -4826,7 +5698,7 @@ Changelog of JSqlParser. **implement feature (#972)** -* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> +* Co-authored-by: Jan Monterrubio <Jan.Monterrubio@Cerner.com> [aee39](https://github.com/JSQLParser/JSqlParser/commit/aee3947757eecf4) Jan Monterrubio *2020-04-17 21:01:22* @@ -4942,7 +5814,7 @@ Changelog of JSqlParser. **fixes #936** -* fixes #938 +* fixes #938 [39e92](https://github.com/JSQLParser/JSqlParser/commit/39e920df15fefd7) wumpz *2020-02-01 00:08:17* @@ -5048,7 +5920,7 @@ Changelog of JSqlParser. **fixes #899** -* switched to assertj from hamcrest +* switched to assertj from hamcrest [9707e](https://github.com/JSQLParser/JSqlParser/commit/9707e4f0aacff16) wumpz *2019-11-23 23:18:56* @@ -5069,8 +5941,8 @@ Changelog of JSqlParser. **Adding support for STRAIGHT_JOIN in the select clause (#861)** -* Adding support for straight_join in the select clause -* Renaming the field name to reflect that this is a MySQL hint +* Adding support for straight_join in the select clause +* Renaming the field name to reflect that this is a MySQL hint [3cdea](https://github.com/JSQLParser/JSqlParser/commit/3cdea6bd3d9ce21) Tomer Shay (Shimshi) *2019-11-09 20:30:54* @@ -5121,10 +5993,10 @@ Changelog of JSqlParser. **Added support for Oracle UNPIVOT keyword. (#882)** -* Added support for Oracle UNPIVOT keyword. -* Back to original version number. -* Updated imports. -* Added missing import. +* Added support for Oracle UNPIVOT keyword. +* Back to original version number. +* Updated imports. +* Added missing import. [bcc27](https://github.com/JSQLParser/JSqlParser/commit/bcc271870ad767f) Pascal Mulder *2019-10-26 20:35:43* @@ -5285,7 +6157,7 @@ Changelog of JSqlParser. **fixes #648** -* fixes #638 +* fixes #638 [74e02](https://github.com/JSQLParser/JSqlParser/commit/74e02267404da4e) wumpz *2019-08-13 19:19:12* @@ -5631,7 +6503,7 @@ Changelog of JSqlParser. **Support KSQL's WINDOW** -* Add support for KSQL's WINDOW (HOPPING, TUMBLING and SESSION window) +* Add support for KSQL's WINDOW (HOPPING, TUMBLING and SESSION window) [ef911](https://github.com/JSQLParser/JSqlParser/commit/ef9119806146f25) Suyash Garg *2019-06-21 12:00:06* @@ -5757,7 +6629,7 @@ Changelog of JSqlParser. **tests #775** -* removed some not flags from some classes +* removed some not flags from some classes [8dda4](https://github.com/JSQLParser/JSqlParser/commit/8dda4a60a8e558d) wumpz *2019-04-08 21:36:40* @@ -5833,7 +6705,7 @@ Changelog of JSqlParser. **update README.md (#762)** -* update latest version(1.4) +* update latest version(1.4) [13d6a](https://github.com/JSQLParser/JSqlParser/commit/13d6a9fe183a5ab) r548 *2019-03-14 12:19:58* @@ -5889,7 +6761,7 @@ Changelog of JSqlParser. **fixes #649** -* and implemented ! for not and extended not expression +* and implemented ! for not and extended not expression [10e8e](https://github.com/JSQLParser/JSqlParser/commit/10e8e2568eb7711) wumpz *2019-02-23 23:32:38* @@ -5935,39 +6807,39 @@ Changelog of JSqlParser. **Added support for DROP INDEX, ADD UNIQUE INDEX, ALGORITHM and USING (#752)** -* Merge recent changes in the master from the master (#1) -* changed license header to represent the projects dual license -* changed license header to represent the projects dual license -* changed license header to represent the projects dual license -* changed license header to represent the projects dual license -* Added support for comment(s) for column definitions in CREATE TABLE s… (#743) -* Added support for comment(s) for column definitions in CREATE TABLE statements -* Added support for comment(s) for column definitions in CREATE TABLE statements #2 -* To increase code coverage -* To increase code coverage #2 -* Added support for 'ALTER TABLE CHANGE COLUMN' (#741) -* Added support for 'ALTER TABLE CHANGE COLUMN oldName newName columnDefinition'. Please see https://dev.mysql.com/doc/refman/8.0/en/alter-table.html for reference. -* Returned import ordering to avoid conflicts -* Improved the tests somewhat -* Now also test the getOptionalSpecifier() for both cases (null and not-null) -* Expanded tests for ALTER TABLE ... CHANGE -* implemented optimize for, fixes #348 -* implemented optimize for, fixes #348 -* Support for simple informix outer joins. (#745) -* added support for simple informix outer joins -* added some test code -* added support for simple informix outer joins -* added some test code -* more testing for better code coverage -* added support for simple informix outer joins -* added some test code -* more testing for better code coverage -* fixes #747 -* fixes #733 -* fixes #707 -* Update README.md -* Update README.md -* Fix handles the following cases: 1) DROP INDEX 2) ADD UNIQUE INDEX 3) ALGORITHM 4) USING <index type> +* Merge recent changes in the master from the master (#1) +* changed license header to represent the projects dual license +* changed license header to represent the projects dual license +* changed license header to represent the projects dual license +* changed license header to represent the projects dual license +* Added support for comment(s) for column definitions in CREATE TABLE s… (#743) +* Added support for comment(s) for column definitions in CREATE TABLE statements +* Added support for comment(s) for column definitions in CREATE TABLE statements #2 +* To increase code coverage +* To increase code coverage #2 +* Added support for 'ALTER TABLE CHANGE COLUMN' (#741) +* Added support for 'ALTER TABLE CHANGE COLUMN oldName newName columnDefinition'. Please see https://dev.mysql.com/doc/refman/8.0/en/alter-table.html for reference. +* Returned import ordering to avoid conflicts +* Improved the tests somewhat +* Now also test the getOptionalSpecifier() for both cases (null and not-null) +* Expanded tests for ALTER TABLE ... CHANGE +* implemented optimize for, fixes #348 +* implemented optimize for, fixes #348 +* Support for simple informix outer joins. (#745) +* added support for simple informix outer joins +* added some test code +* added support for simple informix outer joins +* added some test code +* more testing for better code coverage +* added support for simple informix outer joins +* added some test code +* more testing for better code coverage +* fixes #747 +* fixes #733 +* fixes #707 +* Update README.md +* Update README.md +* Fix handles the following cases: 1) DROP INDEX 2) ADD UNIQUE INDEX 3) ALGORITHM 4) USING <index type> [2830c](https://github.com/JSQLParser/JSqlParser/commit/2830c17ea226635) Prateek Gupta *2019-02-19 00:44:35* @@ -6033,14 +6905,14 @@ Changelog of JSqlParser. **Support for simple informix outer joins. (#745)** -* added support for simple informix outer joins -* added some test code -* added support for simple informix outer joins -* added some test code -* more testing for better code coverage -* added support for simple informix outer joins -* added some test code -* more testing for better code coverage +* added support for simple informix outer joins +* added some test code +* added support for simple informix outer joins +* added some test code +* more testing for better code coverage +* added support for simple informix outer joins +* added some test code +* more testing for better code coverage [53e24](https://github.com/JSQLParser/JSqlParser/commit/53e247bffdae557) Kurt Schwitters *2019-02-08 05:52:51* @@ -6071,20 +6943,20 @@ Changelog of JSqlParser. **Added support for 'ALTER TABLE CHANGE COLUMN' (#741)** -* Added support for 'ALTER TABLE CHANGE COLUMN oldName newName columnDefinition'. Please see https://dev.mysql.com/doc/refman/8.0/en/alter-table.html for reference. -* Returned import ordering to avoid conflicts -* Improved the tests somewhat -* Now also test the getOptionalSpecifier() for both cases (null and not-null) -* Expanded tests for ALTER TABLE ... CHANGE +* Added support for 'ALTER TABLE CHANGE COLUMN oldName newName columnDefinition'. Please see https://dev.mysql.com/doc/refman/8.0/en/alter-table.html for reference. +* Returned import ordering to avoid conflicts +* Improved the tests somewhat +* Now also test the getOptionalSpecifier() for both cases (null and not-null) +* Expanded tests for ALTER TABLE ... CHANGE [bfb80](https://github.com/JSQLParser/JSqlParser/commit/bfb8023318c31b2) Simon *2019-02-07 14:49:28* **Added support for comment(s) for column definitions in CREATE TABLE s… (#743)** -* Added support for comment(s) for column definitions in CREATE TABLE statements -* Added support for comment(s) for column definitions in CREATE TABLE statements #2 -* To increase code coverage -* To increase code coverage #2 +* Added support for comment(s) for column definitions in CREATE TABLE statements +* Added support for comment(s) for column definitions in CREATE TABLE statements #2 +* To increase code coverage +* To increase code coverage #2 [07b86](https://github.com/JSQLParser/JSqlParser/commit/07b86761d0b4aee) Prateek Gupta *2019-02-07 07:09:26* @@ -6165,13 +7037,13 @@ Changelog of JSqlParser. **allow top keyword as column / table / alias name** -* implemented tests +* implemented tests [9e81e](https://github.com/JSQLParser/JSqlParser/commit/9e81e1592200952) wumpz *2019-01-23 23:00:12* **allow top keyword as column / table / alias name** -* implemented tests +* implemented tests [ed95e](https://github.com/JSQLParser/JSqlParser/commit/ed95e877802f46e) wumpz *2019-01-23 22:58:39* @@ -6197,7 +7069,7 @@ Changelog of JSqlParser. **corrected stackoverflow while tables extraction** -* updated readme +* updated readme [04db1](https://github.com/JSQLParser/JSqlParser/commit/04db124b85dea22) wumpz *2019-01-20 22:33:51* @@ -6208,7 +7080,7 @@ Changelog of JSqlParser. **started describe** -* some cleanup +* some cleanup [25fa3](https://github.com/JSQLParser/JSqlParser/commit/25fa31153a9a2d0) wumpz *2019-01-20 21:48:12* @@ -6224,7 +7096,7 @@ Changelog of JSqlParser. **corrected some failing tests** -* included a regression test for oracle files +* included a regression test for oracle files [aa932](https://github.com/JSQLParser/JSqlParser/commit/aa932c4a28d8021) wumpz *2018-12-30 23:06:59* @@ -6235,8 +7107,8 @@ Changelog of JSqlParser. **Support Alter Table Drop Constraint If Exists (#709)** -* Support Alter Table Drop Constraint If Exists -* #709 add constraintIfExists flag +* Support Alter Table Drop Constraint If Exists +* #709 add constraintIfExists flag [08fed](https://github.com/JSQLParser/JSqlParser/commit/08fedf752a3f99d) Robert Scholte *2018-12-13 07:14:10* @@ -6247,12 +7119,12 @@ Changelog of JSqlParser. **Support KSQL's WITHIN (#722)** -* Implements WITHIN for KSQL windowed joins -* Clean up -* Improve test -* Implements WITHIN ( before TimeUnit, after TimeUnit ) for KSQL -* Also restricts TimeUnit to units accepted by KSQL -* WITHIN should come before ON +* Implements WITHIN for KSQL windowed joins +* Clean up +* Improve test +* Implements WITHIN ( before TimeUnit, after TimeUnit ) for KSQL +* Also restricts TimeUnit to units accepted by KSQL +* WITHIN should come before ON [ae665](https://github.com/JSQLParser/JSqlParser/commit/ae665e60655f45f) Lionel Montrieux *2018-12-11 21:53:07* @@ -6393,7 +7265,7 @@ Changelog of JSqlParser. **fixes #670** -* added testcase, corrected deparser +* added testcase, corrected deparser [455c5](https://github.com/JSQLParser/JSqlParser/commit/455c5f50671eed9) wumpz *2018-09-10 15:38:54* @@ -6429,7 +7301,7 @@ Changelog of JSqlParser. **Update README.md (#667)** -* Fix a couple typos/grammar issues +* Fix a couple typos/grammar issues [a822f](https://github.com/JSQLParser/JSqlParser/commit/a822fead1b21793) Kai Presler-Marshall *2018-08-29 13:11:41* @@ -6475,10 +7347,10 @@ Changelog of JSqlParser. **Parse Cloud Spanner raw string and byte prefixes (#659)** -* #656 parse cloud spanner raw string and byte literals -* #656 fixed raw byte string prefix -* #656 fixed test case -* fixed reported codacy issue +* #656 parse cloud spanner raw string and byte literals +* #656 fixed raw byte string prefix +* #656 fixed test case +* fixed reported codacy issue [a57db](https://github.com/JSQLParser/JSqlParser/commit/a57db5d031a5229) Knut Olav Løite *2018-08-14 08:12:26* @@ -6704,13 +7576,13 @@ Changelog of JSqlParser. **- allow parenthesis around from item** -* - allow whitespace between bars from concat +* - allow whitespace between bars from concat [2cea3](https://github.com/JSQLParser/JSqlParser/commit/2cea3ee94d83447) wumpz *2018-03-29 22:25:48* **- allow parenthesis around from item** -* - allow whitespace between bars from concat +* - allow whitespace between bars from concat [dabb0](https://github.com/JSQLParser/JSqlParser/commit/dabb03f0094e4c7) wumpz *2018-03-29 22:24:33* @@ -6731,8 +7603,8 @@ Changelog of JSqlParser. **JSQLPARSER-584: adds support for MySQL (a,b,...)OP(c,d,...) expression (#585)** -* JSQLPARSER-584: adds support for MySQL (a,b,...)OP(c,d,...) expression -* JSQLPARSER-584: adds some tests and rename MySQLValueListExpression to ValueListExpression +* JSQLPARSER-584: adds support for MySQL (a,b,...)OP(c,d,...) expression +* JSQLPARSER-584: adds some tests and rename MySQLValueListExpression to ValueListExpression [2c272](https://github.com/JSQLParser/JSqlParser/commit/2c272f440995b2c) Adrien Lesur *2018-03-05 23:08:55* @@ -6778,14 +7650,14 @@ Changelog of JSqlParser. **Fix issue #563: subjoin allows only one inner join, this should be a … (#564)** -* Fix issue #563: subjoin allows only one inner join, this should be a list -* Fix failing Oracle tests because of confusion between subjoin and subselect. +* Fix issue #563: subjoin allows only one inner join, this should be a list +* Fix failing Oracle tests because of confusion between subjoin and subselect. [8456a](https://github.com/JSQLParser/JSqlParser/commit/8456acf7f228a46) Frits Jalvingh *2018-02-02 10:40:01* **fixes #320 (#576)** -* fixes #320 +* fixes #320 [e6451](https://github.com/JSQLParser/JSqlParser/commit/e645193140f4271) Taner Mansur *2018-02-01 14:55:37* @@ -6881,7 +7753,7 @@ Changelog of JSqlParser. **Added support for RLIKE expressions (#544)** -* RLIKE is a synonym of REGEXP, therefore should be treated the same. +* RLIKE is a synonym of REGEXP, therefore should be treated the same. [8a950](https://github.com/JSQLParser/JSqlParser/commit/8a950b3e09dce06) sh-tomer *2017-11-16 22:10:51* @@ -6912,12 +7784,12 @@ Changelog of JSqlParser. **Add ability to support "NOT LIKE ..." expressions (#539)** -* The parser is able to parse expressions such as "a NOT LIKE '%pattern%'", but is not able to parse expressions where the not is before the entire expression. For example: "NOT a LIKE '%pattern%'. -* When parsing the latter, the error is: -* Caused by: net.sf.jsqlparser.parser.ParseException: Encountered " "LIKE" "LIKE "" at line 1, column 32. -* Was expecting one of: ... -* The reason this is important is both because these syntaxes are both valid, and also because the deparser uses the second method. -* Therefore, if you parse a query with the first type of expression, then deparse it and parse again, you'll get the same error. +* The parser is able to parse expressions such as "a NOT LIKE '%pattern%'", but is not able to parse expressions where the not is before the entire expression. For example: "NOT a LIKE '%pattern%'. +* When parsing the latter, the error is: +* Caused by: net.sf.jsqlparser.parser.ParseException: Encountered " "LIKE" "LIKE "" at line 1, column 32. +* Was expecting one of: ... +* The reason this is important is both because these syntaxes are both valid, and also because the deparser uses the second method. +* Therefore, if you parse a query with the first type of expression, then deparse it and parse again, you'll get the same error. [daea3](https://github.com/JSQLParser/JSqlParser/commit/daea33e73aa05c6) sh-tomer *2017-10-31 20:27:50* @@ -6938,23 +7810,23 @@ Changelog of JSqlParser. **Linking structures to their AST nodes to have access to their positions (#534)** -* Linking several structures to their AST nodes to have access to their positions -* This far there were only 3 types of structures linked to their AST nodes. Now adding some more expressions and literals to their AST node to have access to their token's position in the query. -* Added missing parts in JSQqlParserCC.jjt for AST linking to work -* Added missing parts in JSQqlParserCC.jjt to make sure all relevant code is created to generate and link AST nodes to the relevant structures. +* Linking several structures to their AST nodes to have access to their positions +* This far there were only 3 types of structures linked to their AST nodes. Now adding some more expressions and literals to their AST node to have access to their token's position in the query. +* Added missing parts in JSQqlParserCC.jjt for AST linking to work +* Added missing parts in JSQqlParserCC.jjt to make sure all relevant code is created to generate and link AST nodes to the relevant structures. [514f2](https://github.com/JSQLParser/JSqlParser/commit/514f2588af97345) sh-tomer *2017-10-29 12:39:56* **add debug note (#531)** -* added a link to the visualize parsing section to have a visible debug mode (so users that create an issue can try to get us better output) +* added a link to the visualize parsing section to have a visible debug mode (so users that create an issue can try to get us better output) [b1abc](https://github.com/JSQLParser/JSqlParser/commit/b1abc6ff39e9c2a) Jan Monterrubio *2017-10-25 06:21:26* **fixes #525 (#530)** -* fixes #525 -* Simply unit test. +* fixes #525 +* Simply unit test. [1a1a1](https://github.com/JSQLParser/JSqlParser/commit/1a1a1aa53866787) Linyu Chen *2017-10-24 05:30:39* @@ -6965,10 +7837,10 @@ Changelog of JSqlParser. **Implements #509 (#504)** -* Supporting MySql hit SQL_CALC_FOUND_ROWS for selecting row count. -* Supporting MySql hit SQL_CALC_FOUND_ROWS for selecting row count. - refactoring -* Supporting MySql hit SQL_CALC_FOUND_ROWS for selecting row count. - missing copyright.ˆ -* Supporting MySql hit SQL_CALC_FOUND_ROWS for selecting row count. - Modify field type to boolean for prevent memory consumption by creating object and try assertSqlCanBeParsedAndDeparsed on unit test. +* Supporting MySql hit SQL_CALC_FOUND_ROWS for selecting row count. +* Supporting MySql hit SQL_CALC_FOUND_ROWS for selecting row count. - refactoring +* Supporting MySql hit SQL_CALC_FOUND_ROWS for selecting row count. - missing copyright.ˆ +* Supporting MySql hit SQL_CALC_FOUND_ROWS for selecting row count. - Modify field type to boolean for prevent memory consumption by creating object and try assertSqlCanBeParsedAndDeparsed on unit test. [3e163](https://github.com/JSQLParser/JSqlParser/commit/3e16345815e45b5) Yoon Kyong Sik *2017-10-20 07:27:48* @@ -6999,7 +7871,7 @@ Changelog of JSqlParser. **fixes #519** -* fixes #520 +* fixes #520 [27217](https://github.com/JSQLParser/JSqlParser/commit/272177a37b9ee81) wumpz *2017-10-06 08:24:04* @@ -7160,11 +8032,11 @@ Changelog of JSqlParser. **fix issue #424 (INSERT with SET) (#481)** -* update insert with set language -* update insert with set -* update insert with set -* update insert test -* add removed lines +* update insert with set language +* update insert with set +* update insert with set +* update insert test +* add removed lines [ca653](https://github.com/JSQLParser/JSqlParser/commit/ca6538a04dd7969) messfish *2017-07-28 06:23:33* @@ -7210,21 +8082,21 @@ Changelog of JSqlParser. **Add Upsert Grammer (#460)** -* Add files via upload -* Add files via upload -* Add files via upload -* Add files via upload -* Add files via upload -* Add test for de parser -* Add files via upload -* Add files via upload -* Add files via upload -* Add files via upload -* Add files via upload -* Add files via upload -* Add files via upload -* Add files via upload -* Add files via upload +* Add files via upload +* Add files via upload +* Add files via upload +* Add files via upload +* Add files via upload +* Add test for de parser +* Add files via upload +* Add files via upload +* Add files via upload +* Add files via upload +* Add files via upload +* Add files via upload +* Add files via upload +* Add files via upload +* Add files via upload [aaeb8](https://github.com/JSQLParser/JSqlParser/commit/aaeb8dfeb0c2a4e) messfish *2017-06-11 18:48:53* @@ -7250,25 +8122,25 @@ Changelog of JSqlParser. **Fix issue #442 (#451)** -* Fix issue #442 for delete statements -* Fix issue #442 for insert statements -* Mock only when necessary -* E.g., interfaces, behavior needs to be verified, etc. -* Prefer the first style of testing -* As discussed in issue #442 -* Fix issue #442 for replace statements -* Improve readability of issue #442 tests -* Inject SelectDeParser as well -* As discussed in issue #442. -* Fix issue #442 for select statements -* Fix issue #442 for update statements -* Fix issue #442 for execute statements -* Fix issue #442 for set statements -* Fix PR code review issue -* https://www.codacy.com/app/wumpz/JSqlParser/file/6682733346/issues/source?bid=4162857&fileBranchId=4580866#l48 -* Skip PMD check for asserts in tests using Mockito -* As agreed upon in the discussion in PR #451. -* Use correct PMD check name +* Fix issue #442 for delete statements +* Fix issue #442 for insert statements +* Mock only when necessary +* E.g., interfaces, behavior needs to be verified, etc. +* Prefer the first style of testing +* As discussed in issue #442 +* Fix issue #442 for replace statements +* Improve readability of issue #442 tests +* Inject SelectDeParser as well +* As discussed in issue #442. +* Fix issue #442 for select statements +* Fix issue #442 for update statements +* Fix issue #442 for execute statements +* Fix issue #442 for set statements +* Fix PR code review issue +* https://www.codacy.com/app/wumpz/JSqlParser/file/6682733346/issues/source?bid=4162857&fileBranchId=4580866#l48 +* Skip PMD check for asserts in tests using Mockito +* As agreed upon in the discussion in PR #451. +* Use correct PMD check name [9d680](https://github.com/JSQLParser/JSqlParser/commit/9d680a6ec4b9f07) chrycheng *2017-05-22 07:13:41* @@ -7279,8 +8151,8 @@ Changelog of JSqlParser. **Fix issue #446 (#447)** -* Test current behavior of ExecuteDeParser -* Fix issue #446 +* Test current behavior of ExecuteDeParser +* Fix issue #446 [275fb](https://github.com/JSQLParser/JSqlParser/commit/275fbbe87fa0ada) chrycheng *2017-05-16 20:46:29* @@ -7316,45 +8188,45 @@ Changelog of JSqlParser. **conversion to CNF (#434)** -* Add files via upload -* Create foo -* All the files needed for the CNF conversion -* Delete foo -* Create foo -* Test cases for the CNF conversion -* Delete foo -* Add files via upload -* Add files via upload -* change some public methods to private -* Delete CNFConverter.java -* Delete CloneHelper.java -* Delete MultiAndExpression.java -* Delete MultiOrExpression.java -* Delete MultipleExpression.java -* Create foo -* Add files via upload -* Delete CNFTest.java -* Delete StepLastHelper.java -* Create foo -* Add files via upload -* Delete foo -* Delete foo -* Add files via upload -* Add files via upload -* Delete CNFConverter.java -* Delete CloneHelper.java -* Delete MultiAndExpression.java -* Delete MultiOrExpression.java -* Delete MultipleExpression.java -* Create foo -* Add files via upload -* Delete foo -* Delete CNFTest.java -* Create foo -* Add files via upload -* Delete foo -* Add files via upload -* Add files via upload +* Add files via upload +* Create foo +* All the files needed for the CNF conversion +* Delete foo +* Create foo +* Test cases for the CNF conversion +* Delete foo +* Add files via upload +* Add files via upload +* change some public methods to private +* Delete CNFConverter.java +* Delete CloneHelper.java +* Delete MultiAndExpression.java +* Delete MultiOrExpression.java +* Delete MultipleExpression.java +* Create foo +* Add files via upload +* Delete CNFTest.java +* Delete StepLastHelper.java +* Create foo +* Add files via upload +* Delete foo +* Delete foo +* Add files via upload +* Add files via upload +* Delete CNFConverter.java +* Delete CloneHelper.java +* Delete MultiAndExpression.java +* Delete MultiOrExpression.java +* Delete MultipleExpression.java +* Create foo +* Add files via upload +* Delete foo +* Delete CNFTest.java +* Create foo +* Add files via upload +* Delete foo +* Add files via upload +* Add files via upload [afe10](https://github.com/JSQLParser/JSqlParser/commit/afe1011bdd64696) messfish *2017-05-07 20:21:00* @@ -7385,10 +8257,10 @@ Changelog of JSqlParser. **Introduce support for mysql index hints (fixing issue #374) (#429)** -* Introduce support for mysql index hints (fixing issue #374) -* Fix checkstyle errors -* -Converted indent tabs to spaces -* -Added missing {} on single-line if statement +* Introduce support for mysql index hints (fixing issue #374) +* Fix checkstyle errors +* -Converted indent tabs to spaces +* -Added missing {} on single-line if statement [6db15](https://github.com/JSQLParser/JSqlParser/commit/6db15d2c6218d06) Joey Mart *2017-04-18 06:22:25* @@ -7399,8 +8271,8 @@ Changelog of JSqlParser. **Addressing #427 (#428)** -* updating readme with Maven requirements -* removing ticks +* updating readme with Maven requirements +* removing ticks [0fddc](https://github.com/JSQLParser/JSqlParser/commit/0fddc73e2eee1b2) AnEmortalKid *2017-04-17 23:39:05* @@ -7511,8 +8383,8 @@ Changelog of JSqlParser. **Fix #407 by enhancing grammar (#410)** -* Fix #407 by enhancing grammar -* Change LF and tabs +* Fix #407 by enhancing grammar +* Change LF and tabs [5d901](https://github.com/JSQLParser/JSqlParser/commit/5d9018657df6b22) Christophe Moine *2017-03-22 07:36:14* @@ -7583,19 +8455,19 @@ Changelog of JSqlParser. **Support FOR UPDATE WAIT (#405)** -* Adding FOR UPDATE WAIT support -* removing final peppered everywhere -* updating formatting, fixing codacy test names -* updating asserts to use static import -* reverting changes -* reverting line feeds -* adding tests and deparser code back without formatting +* Adding FOR UPDATE WAIT support +* removing final peppered everywhere +* updating formatting, fixing codacy test names +* updating asserts to use static import +* reverting changes +* reverting line feeds +* adding tests and deparser code back without formatting [45b39](https://github.com/JSQLParser/JSqlParser/commit/45b392f4b93714c) AnEmortalKid *2017-03-10 22:06:53* **add support for LIMIT with only one row count JDBC parameter (#404)** -* small but powerfull change 👍 +* small but powerfull change 👍 [42318](https://github.com/JSQLParser/JSqlParser/commit/42318531bb72279) zhushaoping *2017-03-03 07:06:08* @@ -7696,22 +8568,22 @@ Changelog of JSqlParser. **Increase test coverage on AlterExpression.java** -* getOperation -* getFkColumns -* getFkSourceTable -* getFkSourceColumns -* getConstraintName -* tried getPkColumns but it does not behave as I expected. -* placed TODO in AlterTest.testAlterTablePK for this -* getIndex().getColumnNames +* getOperation +* getFkColumns +* getFkSourceTable +* getFkSourceColumns +* getConstraintName +* tried getPkColumns but it does not behave as I expected. +* placed TODO in AlterTest.testAlterTablePK for this +* getIndex().getColumnNames [5a799](https://github.com/JSQLParser/JSqlParser/commit/5a79964714b13f7) jthomas *2017-01-19 19:43:24* **Enhance AlterExpression grammar:** -* 1. optional "COLUMN" keyword in ADD alter operation -* 2. new alter operation: MODIFY -* 3. add column specs to alter table column definitions +* 1. optional "COLUMN" keyword in ADD alter operation +* 2. new alter operation: MODIFY +* 3. add column specs to alter table column definitions [24177](https://github.com/JSQLParser/JSqlParser/commit/241779b26973b47) jthomas *2017-01-19 17:32:21* @@ -7742,7 +8614,7 @@ Changelog of JSqlParser. **fixes #375** -* fixes #371 +* fixes #371 [957f3](https://github.com/JSQLParser/JSqlParser/commit/957f39c496a7fac) wumpz *2017-01-02 13:30:09* @@ -7848,7 +8720,7 @@ Changelog of JSqlParser. **#modify net.sf.jsqlparser.statement.insert.Insert Method:toString() if itemsList and useSelectBrackets together not null will error** -* #modify PlainSelect.getStringList change sql append to StringBuilder +* #modify PlainSelect.getStringList change sql append to StringBuilder [471b9](https://github.com/JSQLParser/JSqlParser/commit/471b94495623ee2) zheng.liu@baifendian.com *2016-08-15 06:22:23* @@ -7864,21 +8736,21 @@ Changelog of JSqlParser. **Update Alter and add AlterExpression class for multiple ADD/DROP expressions in a single ALTER statement** -* Update .jjt file to break out the AlterExpression into its own class and for Alter to compose multiple AlterExpressions -* Update AlterTest for new AlterExpressions and test for multiple ADD/DROP statements in a single ALTER +* Update .jjt file to break out the AlterExpression into its own class and for Alter to compose multiple AlterExpressions +* Update AlterTest for new AlterExpressions and test for multiple ADD/DROP statements in a single ALTER [e8d1c](https://github.com/JSQLParser/JSqlParser/commit/e8d1cf2cb1db16c) Rob Story *2016-08-11 19:24:41* **Support for parse delete from table using join to another table like:** -* DELETE posts -* FROM posts -* INNER JOIN projects ON projects.project_id = posts.project_id -* WHERE projects.client_id = :client_id -* This necessitated some changes to the DeleteTest class, -* specifically: -* JSqlParserCC.jjt - changes on grammar of Delete statements. -* Delete toString and DeleteDeParser. +* DELETE posts +* FROM posts +* INNER JOIN projects ON projects.project_id = posts.project_id +* WHERE projects.client_id = :client_id +* This necessitated some changes to the DeleteTest class, +* specifically: +* JSqlParserCC.jjt - changes on grammar of Delete statements. +* Delete toString and DeleteDeParser. [b6eb5](https://github.com/JSQLParser/JSqlParser/commit/b6eb57b61f7b205) Lucas Oliveira *2016-08-06 16:28:11* @@ -7979,7 +8851,7 @@ Changelog of JSqlParser. **fixes #296** -* refactored getTableList method of TableNamesFinder +* refactored getTableList method of TableNamesFinder [3ccca](https://github.com/JSQLParser/JSqlParser/commit/3ccca01bbc85778) wumpz *2016-06-28 12:16:39* @@ -8045,12 +8917,12 @@ Changelog of JSqlParser. **Implemented table check constraint for named constraints.** -* 1. Added named constraint to create table. -* 2. Added check constraint to alter table statement. -* 3. Added CheckConstraint type. -* Tests: -* 4. Added create table test. -* 5. Added alter table test. +* 1. Added named constraint to create table. +* 2. Added check constraint to alter table statement. +* 3. Added CheckConstraint type. +* Tests: +* 4. Added create table test. +* 5. Added alter table test. [401d2](https://github.com/JSQLParser/JSqlParser/commit/401d279ef7e6ef9) Megan Woods *2016-06-16 14:56:01* @@ -8066,13 +8938,13 @@ Changelog of JSqlParser. **Implemented:** -* 1. UPDATE .. RETURNING col, col as Alias -* 2. UPDATE .. RETURNING * -* Tested: -* 3. UPDATE .. ORDER BY .. LIMIT .. RETURNING -* 4. UPDATE .. RETURNING -* Item 4 represents the PostgreSQL UPDATE .. RETURNING Syntax without ORDER BY and LIMIT. -* See: https://www.postgresql.org/docs/9.5/static/sql-update.html +* 1. UPDATE .. RETURNING col, col as Alias +* 2. UPDATE .. RETURNING * +* Tested: +* 3. UPDATE .. ORDER BY .. LIMIT .. RETURNING +* 4. UPDATE .. RETURNING +* Item 4 represents the PostgreSQL UPDATE .. RETURNING Syntax without ORDER BY and LIMIT. +* See: https://www.postgresql.org/docs/9.5/static/sql-update.html [f4526](https://github.com/JSQLParser/JSqlParser/commit/f452638f0f04f99) Megan Woods *2016-06-15 09:12:14* @@ -8133,39 +9005,39 @@ Changelog of JSqlParser. **Added ability to have operators like '>=' or '<=' separated by a space.** -* This includes: -* Modifying the JJT syntax to support the 'space in the middle' versions -* of operators (any quantity of whitespace is supported). -* Modifying the various operators to inherit from a new -* 'ComparisonOperator' class, which handles the (previously NotEqualsTo- -* only) logic for capturing the form of the operator. -* Giving each of the various operators a constructor that accepts the -* syntax used. -* Modifying TestUtils to strip comments out before comparing SQL text -* (necessary because condition07.sql is now passing, and has a comment). -* Updating SpecialOracleTest to indicate 130 tests passing now -* (condition7.sql now passes). -* Adding a new test specifically for operators into SpecialOracleTest. -* NOTE: Because the "! =" form of the 'not equals' operator means something -* different in PostgresSQL (factorial of previous argument + equals), we do -* NOT include that case. +* This includes: +* Modifying the JJT syntax to support the 'space in the middle' versions +* of operators (any quantity of whitespace is supported). +* Modifying the various operators to inherit from a new +* 'ComparisonOperator' class, which handles the (previously NotEqualsTo- +* only) logic for capturing the form of the operator. +* Giving each of the various operators a constructor that accepts the +* syntax used. +* Modifying TestUtils to strip comments out before comparing SQL text +* (necessary because condition07.sql is now passing, and has a comment). +* Updating SpecialOracleTest to indicate 130 tests passing now +* (condition7.sql now passes). +* Adding a new test specifically for operators into SpecialOracleTest. +* NOTE: Because the "! =" form of the 'not equals' operator means something +* different in PostgresSQL (factorial of previous argument + equals), we do +* NOT include that case. [9886b](https://github.com/JSQLParser/JSqlParser/commit/9886b02975d1749) Dave Lindquist *2016-04-28 13:28:58* **Corrected "MERGE INTO" parsing for more complicated statements.** -* Specifically: -* Changed "Condition" to "Expression" for the "ON" clause -- this is -* needed to handle "ON" clauses that have "a = y AND b = z" or other -* more complicated expressions (basically the same as the "ON" clause -* in a SELECT query). -* Also changed the "WHERE" and "DELETE WHERE" clauses in the same -* fashion ('Condition' becomes 'Expression'), as they too support -* multiple conditions. -* Corrected the toString on the MergeUpdate clause, which was missing a -* comma between the fields. -* Added a new, more complicated MERGE INTO statement to the MergeTest -* class. +* Specifically: +* Changed "Condition" to "Expression" for the "ON" clause -- this is +* needed to handle "ON" clauses that have "a = y AND b = z" or other +* more complicated expressions (basically the same as the "ON" clause +* in a SELECT query). +* Also changed the "WHERE" and "DELETE WHERE" clauses in the same +* fashion ('Condition' becomes 'Expression'), as they too support +* multiple conditions. +* Corrected the toString on the MergeUpdate clause, which was missing a +* comma between the fields. +* Added a new, more complicated MERGE INTO statement to the MergeTest +* class. [7efd5](https://github.com/JSQLParser/JSqlParser/commit/7efd58f7704a0ff) Dave Lindquist *2016-04-28 13:19:34* @@ -8196,7 +9068,7 @@ Changelog of JSqlParser. **fixes #240** -* fixes #241 +* fixes #241 [7f8b5](https://github.com/JSQLParser/JSqlParser/commit/7f8b59b31e44521) wumpz *2016-04-05 06:25:57* @@ -8372,7 +9244,7 @@ Changelog of JSqlParser. **added TableFunction alias suppurt** -* added TableFunction unit tests +* added TableFunction unit tests [88edd](https://github.com/JSQLParser/JSqlParser/commit/88eddaf50dada22) tfedkiv *2015-12-07 15:27:02* @@ -8423,8 +9295,8 @@ Changelog of JSqlParser. **Issue 195:** -* Add support for ORDER BY and LIMIT in UPDATE and DELETE statements (as supported by MySQL). -* LimitDeparser and OrderByDeParser have been pulled out into separate classes to avoid code duplication from SelectDeParser. +* Add support for ORDER BY and LIMIT in UPDATE and DELETE statements (as supported by MySQL). +* LimitDeparser and OrderByDeParser have been pulled out into separate classes to avoid code duplication from SelectDeParser. [a3133](https://github.com/JSQLParser/JSqlParser/commit/a31333a65141e27) James Heather *2015-11-24 14:57:04* @@ -8490,7 +9362,7 @@ Changelog of JSqlParser. **Support for alter table drop column/constraint** -* Fix for Issue #184 +* Fix for Issue #184 [58dd2](https://github.com/JSQLParser/JSqlParser/commit/58dd28aaf9b9273) schweighart *2015-10-20 21:34:51* @@ -8661,13 +9533,13 @@ Changelog of JSqlParser. **Add support for variable support to "SELECT SKIP FIRST ..." construct** -* The grammar for the construct in informix [1] mentions the possibility, that <ROWCOUNT> can be either -* an integer or a host variable or local SPL variable storing the value of max. The case for plain integers and -* jdbc variables is covered by the first commit While this commit adds support for constructs using SPL -* variables. SPL variables must follow identifier rules [2][3]. -* [1] http://www-01.ibm.com/support/knowledgecenter/SSGU8G_12.1.0/com.ibm.sqls.doc/ids_sqs_0156.htm -* [2] http://www-01.ibm.com/support/knowledgecenter/SSGU8G_12.1.0/com.ibm.sqls.doc/ids_sqs_1306.htm?lang=de -* [3] http://www-01.ibm.com/support/knowledgecenter/SSGU8G_12.1.0/com.ibm.sqls.doc/ids_sqs_1660.htm%23ids_sqs_1660?lang=de +* The grammar for the construct in informix [1] mentions the possibility, that <ROWCOUNT> can be either +* an integer or a host variable or local SPL variable storing the value of max. The case for plain integers and +* jdbc variables is covered by the first commit While this commit adds support for constructs using SPL +* variables. SPL variables must follow identifier rules [2][3]. +* [1] http://www-01.ibm.com/support/knowledgecenter/SSGU8G_12.1.0/com.ibm.sqls.doc/ids_sqs_0156.htm +* [2] http://www-01.ibm.com/support/knowledgecenter/SSGU8G_12.1.0/com.ibm.sqls.doc/ids_sqs_1306.htm?lang=de +* [3] http://www-01.ibm.com/support/knowledgecenter/SSGU8G_12.1.0/com.ibm.sqls.doc/ids_sqs_1660.htm%23ids_sqs_1660?lang=de [9e77b](https://github.com/JSQLParser/JSqlParser/commit/9e77b6bd55ce242) Matthias Bläsing *2015-08-02 14:03:23* @@ -8813,13 +9685,13 @@ Changelog of JSqlParser. **fixes #143** -* some refactorings done +* some refactorings done [4d2a0](https://github.com/JSQLParser/JSqlParser/commit/4d2a0a1151faff1) wumpz *2015-06-05 20:50:28* **fixes #143** -* some refactorings done +* some refactorings done [f71c3](https://github.com/JSQLParser/JSqlParser/commit/f71c307f15c4cde) wumpz *2015-06-05 20:48:45* @@ -9245,8 +10117,8 @@ Changelog of JSqlParser. **Manage OFFSET and FETCH clauses in dedicated classes and rules in** -* jsqlparsercc.jj -* Manage also jdbc parameter in these clauses. +* jsqlparsercc.jj +* Manage also jdbc parameter in these clauses. [26524](https://github.com/JSQLParser/JSqlParser/commit/26524ac850461cb) LionelNirva *2014-10-10 14:38:20* @@ -9257,14 +10129,14 @@ Changelog of JSqlParser. **Add support for new SQL Server 2012 and Oracle 12c versions of LIMIT** -* (equivalent to MySql and PostgreSQL LIMIT ... OFFSET ... clauses) for -* parsing and deparsing. +* (equivalent to MySql and PostgreSQL LIMIT ... OFFSET ... clauses) for +* parsing and deparsing. [dc215](https://github.com/JSQLParser/JSqlParser/commit/dc215bb6b8cf70b) LionelNirva *2014-10-08 12:38:04* **Unit test for the fix Bug when Deparsing SQL Server request having TOP** -* and DISTINCT clauses. +* and DISTINCT clauses. [c2ad9](https://github.com/JSQLParser/JSqlParser/commit/c2ad9d2b2c2ee9f) LionelNirva *2014-10-07 14:31:43* @@ -9285,13 +10157,13 @@ Changelog of JSqlParser. **Fix Bug when Deparsing SQL Server request having TOP and DISTINCT** -* clauses. SQL Server requires the DISTINCT clause to be the first. +* clauses. SQL Server requires the DISTINCT clause to be the first. [7ac70](https://github.com/JSQLParser/JSqlParser/commit/7ac7002d1276f01) LionelNirva *2014-10-03 16:34:49* **Update UpdateTest.java** -* Add an SQL test for Update with Select +* Add an SQL test for Update with Select [aec82](https://github.com/JSQLParser/JSqlParser/commit/aec82516c9d0db6) CeeKayGit *2014-10-01 21:34:51* @@ -9307,31 +10179,31 @@ Changelog of JSqlParser. **Update Update.java** -* Add necessary Select import for Update with Select +* Add necessary Select import for Update with Select [8321a](https://github.com/JSQLParser/JSqlParser/commit/8321aae44a6adce) CeeKayGit *2014-10-01 20:37:42* **Update UpdateDeParser.java** -* Extend the deparser to support DB2 Updates with Select clause +* Extend the deparser to support DB2 Updates with Select clause [3a7bf](https://github.com/JSQLParser/JSqlParser/commit/3a7bf9d520ead88) CeeKayGit *2014-10-01 20:12:14* **Update JSqlParserCC.jj** -* For DB2 "$" is also a standard letter, so handle "$" as #LETTER. +* For DB2 "$" is also a standard letter, so handle "$" as #LETTER. [a6383](https://github.com/JSQLParser/JSqlParser/commit/a6383a52ccee448) CeeKayGit *2014-09-30 20:56:13* **Update Update.java** -* Add support for DB2 Updates with Select clause +* Add support for DB2 Updates with Select clause [1bdb6](https://github.com/JSQLParser/JSqlParser/commit/1bdb69b8891ee26) CeeKayGit *2014-09-30 20:52:21* **Update JSqlParserCC.jj** -* Add support for DB2 Updates with Select clause +* Add support for DB2 Updates with Select clause [17e3d](https://github.com/JSQLParser/JSqlParser/commit/17e3d539a4ce6bd) CeeKayGit *2014-09-30 20:47:54* @@ -9422,7 +10294,7 @@ Changelog of JSqlParser. **Add support for 'UNLOGGED' tables** -* Support the PostgreSQL 9.1+ ‘UNLOGGED’ table feature +* Support the PostgreSQL 9.1+ ‘UNLOGGED’ table feature [eb05c](https://github.com/JSQLParser/JSqlParser/commit/eb05ce30cb90b93) Michaël Cervera *2014-07-27 20:49:00* @@ -9623,7 +10495,7 @@ Changelog of JSqlParser. **corrected some sql test scripts to be deparseable** -* corrected pivot handling in SelectDeParser +* corrected pivot handling in SelectDeParser [8ac25](https://github.com/JSQLParser/JSqlParser/commit/8ac250d9f90144d) wumpz *2014-03-12 23:24:47* @@ -9644,8 +10516,8 @@ Changelog of JSqlParser. **!= usage corrected** -* deparser for oracle hierarchical expressions corrected -* order by asc/desc corrected +* deparser for oracle hierarchical expressions corrected +* order by asc/desc corrected [d4f74](https://github.com/JSQLParser/JSqlParser/commit/d4f744c761fffe9) wumpz *2014-03-06 22:27:11* @@ -9661,9 +10533,9 @@ Changelog of JSqlParser. **toString for windowing elements corrected** -* lax equality test implemented -* included oracle test sqls -* included @ and # for identifiers +* lax equality test implemented +* included oracle test sqls +* included @ and # for identifiers [884dc](https://github.com/JSQLParser/JSqlParser/commit/884dcafa73ecfd2) wumpz *2014-03-05 23:01:03* @@ -9844,8 +10716,8 @@ Changelog of JSqlParser. **Removed the leading and trailing whitespaces in the JavaCC parser file;** -* Organized the declared imports in order to ease further changes in the file and to remove unused ones; -* Renamed the S_INTEGER token to S_LONG to be in sync with the S_DOUBLE token; +* Organized the declared imports in order to ease further changes in the file and to remove unused ones; +* Renamed the S_INTEGER token to S_LONG to be in sync with the S_DOUBLE token; [5d151](https://github.com/JSQLParser/JSqlParser/commit/5d151c7bdbe08fd) Pap Lőrinc *2014-01-28 08:24:04* @@ -10241,7 +11113,7 @@ Changelog of JSqlParser. **create table can now have foreign key definitions** -* fixes #14 +* fixes #14 [0c41f](https://github.com/JSQLParser/JSqlParser/commit/0c41f27165a2f41) wumpz *2013-06-24 23:28:29* @@ -10307,7 +11179,7 @@ Changelog of JSqlParser. **multi value IN expression introduced (a,b,c) in ...** -* fixes #30 +* fixes #30 [3c443](https://github.com/JSQLParser/JSqlParser/commit/3c44391bf5f168f) wumpz *2013-05-26 22:19:03* @@ -10358,8 +11230,8 @@ Changelog of JSqlParser. **- solved critical grammar bug regarding concat expressions and parenthesis parsing** -* - introduced some tests to cover the above -* - corrected ExpressionDeparser to deliver same result as toString for substractions +* - introduced some tests to cover the above +* - corrected ExpressionDeparser to deliver same result as toString for substractions [51365](https://github.com/JSQLParser/JSqlParser/commit/51365239ead790b) wumpz *2013-05-16 21:53:04* @@ -10410,7 +11282,7 @@ Changelog of JSqlParser. **- insertet toString in Update** -* - modified Update deparser to deliver better results +* - modified Update deparser to deliver better results [e2e4d](https://github.com/JSQLParser/JSqlParser/commit/e2e4d80675d525d) wumpz *2013-04-21 21:18:48* @@ -10521,7 +11393,7 @@ Changelog of JSqlParser. **Added automatic license header generation. The link to the original project was moved to the poms description.** -* Format of all sources done. +* Format of all sources done. [01f7a](https://github.com/JSQLParser/JSqlParser/commit/01f7a4c92670794) wumpz *2013-03-19 21:30:50* @@ -10557,7 +11429,7 @@ Changelog of JSqlParser. **- additional test case for values** -* - additional test cases for analytical expressions +* - additional test cases for analytical expressions [15fc8](https://github.com/JSQLParser/JSqlParser/commit/15fc834bf78ae91) wumpz *2013-03-18 23:59:07* @@ -10793,8 +11665,8 @@ Changelog of JSqlParser. **- ExtractExpression integrated** -* - Tests ExtractExpression started -* - Function problem found +* - Tests ExtractExpression started +* - Function problem found [7ea63](https://github.com/JSQLParser/JSqlParser/commit/7ea63d17c1ef299) wumpz *2012-09-16 21:01:53* @@ -10855,14 +11727,14 @@ Changelog of JSqlParser. **quoted columns in create table statement included** -* CreateTableDeParser corrected (NPE with no indexes, toString delivers now same) -* CreateTableTest expanded +* CreateTableDeParser corrected (NPE with no indexes, toString delivers now same) +* CreateTableTest expanded [69287](https://github.com/JSQLParser/JSqlParser/commit/69287d7b2e8b278) wumpz *2012-05-26 19:58:14* **complex with tests included** -* exists formatting included from fork +* exists formatting included from fork [7e39e](https://github.com/JSQLParser/JSqlParser/commit/7e39e9ec4f48448) wumpz *2012-05-23 19:57:10* @@ -10978,7 +11850,7 @@ Changelog of JSqlParser. **Removal of tests that are not able to pass** -* - Functions do not accept conditions as arguments +* - Functions do not accept conditions as arguments [aa5f0](https://github.com/JSQLParser/JSqlParser/commit/aa5f044c5bebc78) Florent Bécart *2011-12-02 07:20:18* @@ -11039,7 +11911,7 @@ Changelog of JSqlParser. **Added support for select without from-list ( SELECT 1 + 2 )** -* Modified test cases for test resources in src/test/resources +* Modified test cases for test resources in src/test/resources [0e5d7](https://github.com/JSQLParser/JSqlParser/commit/0e5d732e65bf622) Christian Bockermann *2011-09-16 20:06:07* @@ -11070,14 +11942,14 @@ Changelog of JSqlParser. **Use of generics** -* - List of expressions in ItemsList -* - List of columns in Statement -* - List of joins in PlainSelect -* - List of withItems in Select -* - List of plainSelects in Union -* - List of orderByElements in Union -* - List of columns in Update -* - List of expressions in Update +* - List of expressions in ItemsList +* - List of columns in Statement +* - List of joins in PlainSelect +* - List of withItems in Select +* - List of plainSelects in Union +* - List of orderByElements in Union +* - List of columns in Update +* - List of expressions in Update [8b0b8](https://github.com/JSQLParser/JSqlParser/commit/8b0b865780ee962) Florent Bécart *2011-06-22 20:00:44* @@ -11103,18 +11975,18 @@ Changelog of JSqlParser. **Eclipse configuration files** -* Makes development environment installation easier +* Makes development environment installation easier [b93af](https://github.com/JSQLParser/JSqlParser/commit/b93af3e4f333cdc) Florent Bécart *2011-06-20 22:33:30* **Add .gitignore file** -* Auto-generated files that won't be pushed to the repo are: -* - classes: compiled main code -* - docs: project javadoc -* - *.jar: distribution jar (sources and documentation) -* - lib: library jar (.class files) -* - testclasses: compiled unit tests +* Auto-generated files that won't be pushed to the repo are: +* - classes: compiled main code +* - docs: project javadoc +* - *.jar: distribution jar (sources and documentation) +* - lib: library jar (.class files) +* - testclasses: compiled unit tests [55197](https://github.com/JSQLParser/JSqlParser/commit/5519799ce41c45b) Florent Bécart *2011-06-20 22:08:46* @@ -11135,6 +12007,8 @@ Changelog of JSqlParser. **Initial commit (base version: 0.7.0)** -* Source: http://sourceforge.net/projects/jsqlparser/files/jsqlparser/jsqlparser-0.7.0.jar/download +* Source: http://sourceforge.net/projects/jsqlparser/files/jsqlparser/jsqlparser-0.7.0.jar/download [67c91](https://github.com/JSQLParser/JSqlParser/commit/67c9150f5d93ced) Florent Bécart *2011-06-20 21:44:37* + + diff --git a/README.md b/README.md index c310ef5452..d4b0e2848a 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ EqualsTo equalsTo = (EqualsTo) select.getWhere(); Column a = (Column) equalsTo.getLeftExpression(); Column b = (Column) equalsTo.getRightExpression(); Assertions.assertEquals("a", a.getColumnName()); -Assertions.assertEquals("b", b.getColumnName()); + Assertions.assertEquals("b", b.getColumnName()); ``` The tree is traversable with the Visitor pattern, and the same object model works in reverse: @@ -106,7 +106,7 @@ Snapshot coordinates and repository setup are on the ## Performance **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. +tested, in any language — 19× ahead of `sqlglot[c]` on JSqlParser's own `SELECT` test suite. And the community kept sending performance improvements since. Thank you everyone!
@@ -115,9 +115,11 @@ tested, in any language — 19× ahead of `sqlglot[c]` on JSqlParser's own `SELE
```text -Benchmark (version) Mode Cnt Score Error Units -JSQLParserBenchmark.parseSQLStatements latest avgt 15 7.602 ± 0.135 ms/op -JSQLParserBenchmark.parseSQLStatements 5.3 avgt 15 84.687 ± 3.321 ms/op +Benchmark (version) Mode Cnt Score Error Units +JSQLParserBenchmark.parseSQLStatements latest avgt 15 21.138 ± 0.557 ms/op <-- Community sponsored improvements! +JSQLParserBenchmark.parseSQLStatements 5.4 avgt 15 32.528 ± 0.666 ms/op +JSQLParserBenchmark.parseSQLStatements 5.3 avgt 15 983.459 ± 8.197 ms/op +JSQLParserBenchmark.parseSQLStatements 5.1 avgt 15 319.601 ± 4.081 ms/op ``` Methodology and the full cross-parser comparison against SQLGlot, `sqlglot[c]` and @@ -138,9 +140,21 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | | Statements | |---|---| | **Queries** | `SELECT` · `WITH …` · Piped SQL | +| **ksqlDB windows** | JOIN `WITHIN`, window `GRACE PERIOD`, and `EMIT CHANGES`/`FINAL` | +| **ClickHouse column selection** | `COLUMNS('regexp')` select items with chained `APPLY`, `EXCEPT`, and `REPLACE` transformers | | **DML** | `INSERT` · `UPDATE` · `UPSERT` · `MERGE` · `DELETE` · `TRUNCATE TABLE` | +| **Oracle RETURNING** | Named output binds (`INTO :name`), JDBC output parameters (`INTO ?`), and `BULK COLLECT INTO` | | **DDL** | `CREATE …` · `ALTER …` · `DROP …` | +| **Oracle vector types** | `VECTOR(*, *)` · `VECTOR(*, FLOAT32)` · `VECTOR(3, *)`, including dense and sparse storage formats | +| **PostgreSQL ownership** | `DROP OWNED` · `REASSIGN OWNED` | +| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` | +| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` | +| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` | | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | +| **Routine changes** | `ALTER FUNCTION` · `ALTER PROCEDURE` · `ALTER ROUTINE` · PostgreSQL `ALTER TRIGGER` | +| **MySQL metadata statements** | `SHOW [FULL] COLUMNS`/`FIELDS` · `SHOW TABLE STATUS` · `SHOW CREATE TABLE`/`VIEW`/`PROCEDURE`/`FUNCTION`/`TRIGGER`/`EVENT`/`DATABASE`/`USER` | +| **PostgreSQL administration** | `CREATE`/`ALTER EVENT TRIGGER` · `REINDEX` · `SECURITY LABEL` | +| **Informix constraints** | `ALTER TABLE … ADD CONSTRAINT` with trailing constraint names for primary, unique, foreign and check constraints; enable with `parser.withDialect(Dialect.INFORMIX)` | | **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` | Beyond statement shapes, the grammar handles nested sub-selects, bind parameters (`?`, @@ -148,6 +162,48 @@ Beyond statement shapes, the grammar handles nested sub-selects, bind parameters array-literal ambiguity. The complete reference is on the [syntax page](https://jsqlparser.github.io/JSqlParser/syntax.html). +PostgreSQL dollar-quoted strings, including `$tag$…$tag$`, retain their delimiter and +literal body in `StringValue`. Tagged quotes are disabled by default to preserve +identifier parsing. Enable them with `parser.withDialect(Dialect.POSTGRESQL)` or +`parser.withDollarQuotedStringTags(true)`. Untagged `$$…$$` literals remain enabled. + +## Statement classification + +Any parsed statement can say what it actually does — no second parse, no visitor to write: + +```java +StatementFeatures features = CCJSqlParserUtil.parse(sqlStr).getFeatures(); + +// safeguard a read-only client before anything reaches the database +if (connection.isReadOnly() && features.mayModifyData()) { + throw new SQLException("rejected: " + features.getUnresolvedReferences()); +} + +// dispatch correctly +if (features.returnsResultSet()) { statement.executeQuery(sqlStr); } +else { statement.executeUpdate(sqlStr); } +``` + +This is not `sqlStr.startsWith("SELECT")` with extra steps. `RETURNING` turns DML into a row +source, a data-modifying CTE hides a `DELETE` inside a `SELECT`, and `INSERT INTO x SELECT ..` +contains a query but returns nothing: + +| SQL | returns rows | modifies data | +|---|---|---| +| `SELECT * FROM t` | yes | no | +| `INSERT INTO x SELECT * FROM t` | no | yes | +| `DELETE FROM t RETURNING *` | yes | yes | +| `WITH c AS (DELETE FROM t RETURNING *) SELECT * FROM c` | yes | yes | +| `SELECT nextval('s')` | yes | *unproven* | + +Features are not mutually exclusive, and each is three-valued: proven, not excludable, or ruled +out. `is()` answers "did the grammar prove it", `may()` answers "could it be ruled out" — so a +guard uses `may()` and a dispatcher uses `is()`. Function volatility is not a syntactic property, +so anything the caller has not declared pure stays unproven and is listed by name. + +Legacy MySQL `GROUP BY ... ASC/DESC` is available with `Dialect.MYSQL` and the explicit +`withLegacyMySqlGroupBy(true)` option; modern/default parsing keeps it disabled. + ## Piped SQL Support is progressing for Piped SQL, which writes queries in the order they actually @@ -167,6 +223,9 @@ Background reading: the [Google research paper](https://storage.googleapis.com/g [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). +ODBC `{fn TIMESTAMPADD(...)}` and `{fn TIMESTAMPDIFF(...)}` expose standard +`SQL_TSI_*` interval arguments as time-unit expressions, preserving column traversal. + ## Java version | JSqlParser | Runtime | Notes | diff --git a/build.gradle b/build.gradle index dffc267ea4..7d041af278 100644 --- a/build.gradle +++ b/build.gradle @@ -165,13 +165,13 @@ dependencies { xmlDoclet ('com.manticore-projects.tools:xml-doclet:+'){ changing = true } // enforce latest version of JavaCC - testImplementation('com.manticore-projects.jsqlformatter:javacc-core:+') - testImplementation('com.manticore-projects.jsqlformatter:javacc-java:+') + testImplementation('com.manticore-projects.jsqlformatter:javacc-core:[8.1.3.1941,)') + testImplementation('com.manticore-projects.jsqlformatter:javacc-java:[8.1.3.1941,)') 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:+') + javacc('com.manticore-projects.jsqlformatter:javacc-core:[8.1.3.1941,)') + javacc('com.manticore-projects.jsqlformatter:javacc-java:[8.1.3.1941,)') } compileJavacc { @@ -276,7 +276,9 @@ test { jvmArgs = ['-Xss4m', '--add-opens=java.base/java.lang=ALL-UNNAMED'] jacoco { - excludes = ['net/sf/jsqlparser/parser/CCJSqlParserTokenManager'] + // the generated parser exceeds the constant-pool limit once instrumented (#2866) + excludes = ['net/sf/jsqlparser/parser/CCJSqlParserTokenManager', + 'net/sf/jsqlparser/parser/CCJSqlParser'] } } @@ -291,13 +293,23 @@ jacocoTestReport { csv.required = false html.outputLocation = layout.buildDirectory.dir('reports/jacoco') } + // the classes that are not instrumented, see test.jacoco + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + "**/parser/CCJSqlParserTokenManager.class", + "**/parser/CCJSqlParser.class" + ]) + })) + } } jacocoTestCoverageVerification { - // Jacoco can't handle the TokenManager class + // Jacoco can't handle the TokenManager and the generated parser classes afterEvaluate { classDirectories.setFrom(files(classDirectories.files.collect { fileTree(dir: it, exclude: [ - "**CCJSqlParserTokenManager**" + "**/parser/CCJSqlParserTokenManager.class", + "**/parser/CCJSqlParser.class" ]) })) } @@ -447,7 +459,9 @@ tasks.register('renderRR') { 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', + def convertProc = new ProcessBuilder('java', + '-Djdk.xml.maxElementDepth=0', + '-jar', new File(rrDir, "convert.war").absolutePath, jjFile) .redirectOutput(ebnfFile) @@ -504,7 +518,7 @@ Version {{name}} {{#commits}} {{#ifMatches messageTitle "^(?!Merge).*"}} * **{{{messageTitle}}}** - + {{authorName}}, {{commitDate}} {{/ifMatches}} {{/commits}} @@ -587,7 +601,9 @@ tasks.register('sphinx', Exec) { } } -publish { +// `publish` is Gradle's own task; the Central Portal upload runs under `publishToMavenCentral`. +// Both need the same prerequisites, or a CI job invoking the latter silently skips check and docs. +tasks.matching { it.name in ['publish', 'publishToMavenCentral'] }.configureEach { dependsOn(check, gitChangelogTask, renderRR, xslt, xmldoc) } @@ -634,17 +650,27 @@ mavenPublishing { } } -// Fix signing task dependencies -tasks.withType(AbstractPublishToMaven).configureEach { - dependsOn(tasks.withType(Sign)) -} - -signing { - required { !version.endsWith("SNAPSHOT") && gradle.taskGraph.hasTask("publish") } -} +// Signing and its task wiring are handled by mavenPublishing.signAllPublications() above. +// The manual `signing { required { ... hasTask("publish") } }` block that used to live here +// never matched the `publishToMavenCentral` task, so the two disagreed about whether to sign. tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' + + // javac's parser is recursive descent: one stack frame per level of nesting. The generated + // CCJSqlParser is nested deeply enough to exhaust the default thread stack, which surfaces as + // "An exception has occurred in the compiler ... java.lang.StackOverflowError" - a compiler + // crash, not a compile error, and not a bug worth reporting to Oracle. + // + // -Xss only takes effect at JVM launch, so the compiler has to run in its own process: + // setting forkOptions without options.fork = true silently does nothing. + options.fork = true + options.forkOptions.jvmArgs = (options.forkOptions.jvmArgs ?: []) + ['-Xss16m'] +} + +// javadoc parses the same sources in a forked process and hits the same wall +tasks.withType(Javadoc).configureEach { + options.setJFlags((options.getJFlags() ?: []) + ['-Xss16m']) } remotes { @@ -696,11 +722,139 @@ jmh { , "-XX:+DebugNonSafepoints" ] - profilers = ['async:libPath=/opt/async-profiler/lib/libasyncProfiler.so;output=tree;dir=build/reports/jmh'] + // Profiling is opt-in because the native library path depends on the host. + profilers = providers.gradleProperty('jmhProfiler').map { [it] }.getOrElse([]) includes = ['.*JSQLParserBenchmark.*'] warmupIterations = 2 fork = 3 iterations = 5 timeOnIteration = '1s' -} \ No newline at end of file +} + +// --------------------------------------------------------------------------------------------- +// Grammar ambiguity gate +// +// JavaCC reports choice conflicts as "Warning: ..." on stdout and still exits 0, so an ambiguity +// introduced by a PR is completely invisible to `./gradlew check`. The `org.javacc.javacc` plugin +// forwards that output to the Gradle logger, where it cannot be inspected either. +// +// This task therefore re-runs JavaCC on the JJTree output, into a throwaway directory, captures +// stdout+stderr and fails when any warning survives the allow list. +// --------------------------------------------------------------------------------------------- + +// Regexes for warnings that are known-benign. Add an entry ONLY together with a comment saying +// why. Never add a "Choice conflict" pattern here — that is the thing this gate exists to catch. +def grammarWarningAllowList = [ + // JJTree copies the options block into the .jj; re-passing -OUTPUT_DIRECTORY on the + // command line makes JavaCC complain that it overrides the in-file value. + ~/^Warning: .*Command line setting of .* modifies option value in file\./, + + // -OUTPUT_DIRECTORY points at a scratch directory that is wiped before every run, so + // JavaCC always announces that it is creating it. Housekeeping, not a grammar problem. + ~/^Warning: Output directory .* does not exist\. Creating the directory\./ +] + +def javaccMainClassOverride = project.findProperty('javaccMainClass') + +tasks.register('checkGrammarAmbiguity') { + group = 'verification' + description = 'Fails the build when JavaCC reports choice conflicts (grammar ambiguities).' + + dependsOn(compileJavacc) + + def jjFile = layout.buildDirectory + .file('generated/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jj') + def scratchDir = layout.buildDirectory.dir('grammar-check') + def reportFile = layout.buildDirectory.file('reports/grammar/javacc-warnings.txt') + def javaccClasspath = configurations.javacc + def allowList = grammarWarningAllowList + def mainClassOverride = javaccMainClassOverride + + inputs.file(jjFile) + inputs.files(javaccClasspath) + outputs.file(reportFile) + + doLast { + def jj = jjFile.get().asFile + if (!jj.exists()) { + throw new GradleException("Generated grammar not found: ${jj} — did compileJavacc run?") + } + + def out = scratchDir.get().asFile + out.deleteDir() + out.mkdirs() + + // JavaCC 8 splits into javacc-core / javacc-java; take Main-Class from whichever jar + // declares one, so this keeps working across upgrades of the Manticore JavaCC branch. + def mainClass = mainClassOverride ?: javaccClasspath.files.findResult { f -> + if (!f.name.endsWith('.jar')) { + return null + } + new java.util.jar.JarFile(f).withCloseable { jar -> + jar.manifest?.mainAttributes?.getValue('Main-Class') + } + } ?: 'org.javacc.parser.Main' + + def isWindows = System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('win') + def javaExe = new File(new File(System.getProperty('java.home'), 'bin'), + isWindows ? 'java.exe' : 'java') + + def proc = new ProcessBuilder( + javaExe.absolutePath, + '-cp', javaccClasspath.asPath, + mainClass.toString(), + '-GRAMMAR_ENCODING=UTF-8', + "-OUTPUT_DIRECTORY=${out.absolutePath}".toString(), + jj.absolutePath) + .directory(out) + .redirectErrorStream(true) + .start() + + def output = proc.inputStream.getText('UTF-8') + def exit = proc.waitFor() + + // Warnings are multi-line: a "Warning:" header followed by indented continuation lines. + def warnings = [] + def current = null + output.eachLine { line -> + if (line.startsWith('Warning:')) { + current = new StringBuilder(line) + warnings << current + } else if (current != null && line.startsWith(' ')) { + current.append('\n').append(line) + } else { + current = null + } + } + + def offending = warnings + .collect { it.toString() } + .findAll { w -> allowList.every { !(w =~ it) } } + + def report = reportFile.get().asFile + report.parentFile.mkdirs() + report.setText(offending.isEmpty() ? 'No JavaCC grammar warnings.\n' + : offending.join('\n\n') + '\n', 'UTF-8') + + if (exit != 0) { + logger.error(output) + throw new GradleException("JavaCC exited with ${exit} while analysing the grammar.") + } + + if (!offending.isEmpty()) { + def conflicts = offending.count { it.contains('Choice conflict') } + logger.error("\n${offending.join('\n\n')}\n") + throw new GradleException( + "${offending.size()} JavaCC grammar warning(s), ${conflicts} of them choice " + + "conflicts. Resolve the ambiguity or add an explicit LOOKAHEAD to the earlier " + + "expansion. Report: ${report}") + } + + logger.lifecycle('Grammar is free of JavaCC choice conflicts.') + } +} + +tasks.named('check') { + dependsOn('checkGrammarAmbiguity') +} diff --git a/gradle.properties b/gradle.properties index 8b590d14c8..4df9d4bd05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx8G -Xss8m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError +org.gradle.jvmargs=-Xmx8G -Xss16m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError org.gradle.caching=true diff --git a/nb-configuration.xml b/nb-configuration.xml index 2c751cae4d..04d5801d39 100644 --- a/nb-configuration.xml +++ b/nb-configuration.xml @@ -19,7 +19,6 @@ LF false true - JDK_11 false none 4 @@ -28,5 +27,6 @@ 120 true project + Temurin_11.0.32_9 diff --git a/pom.xml b/pom.xml index cd1e28b0b6..172bc3eae7 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.github.jsqlparser jsqlparser - 5.4-SNAPSHOT + 5.5-SNAPSHOT bundle JSQLParser library @@ -78,7 +78,7 @@ UTF-8 - 2026-01-01T00:00:00Z + 2026-09-13T21:14:58Z @@ -120,7 +120,7 @@ is expected to track the newest fork build. Pin it (e.g. 8.1.1.1939) before cutting a release so the release is reproducible. --> - [8.1.1.1939,) + [8.1.2.1941,) 5.14.4 5.23.0 3.27.7 @@ -289,7 +289,7 @@ [3.9.0,) - + @@ -426,6 +426,11 @@ jjtree-javacc + -CODE_GENERATOR:"Java" @@ -564,11 +569,13 @@ @{jacocoArgLine} is resolved late, after jacoco:prepare-agent has run. Without it the JaCoCo agent would be dropped and coverage reports empty. --> + @{jacocoArgLine} --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED -Xmx2G -Xms800m -Xss4m + -Dfile.encoding=UTF-8 @@ -612,8 +619,8 @@ *.md .gitignore - - + + true 4 @@ -625,13 +632,13 @@ src/main/java/**/*.java src/test/java/**/*.java - - + + config/formatter/eclipse-java-google-style.xml - + @@ -759,28 +766,28 @@ **/module-info.java,**/net/sf/jsqlparser/parser/SimpleCharStream.java - - + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index ada4bfdf45..14ed418f03 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -25,6 +25,7 @@ exports net.sf.jsqlparser.statement.alter.sequence; exports net.sf.jsqlparser.statement.analyze; exports net.sf.jsqlparser.statement.comment; + exports net.sf.jsqlparser.statement.create.event; exports net.sf.jsqlparser.statement.create.function; exports net.sf.jsqlparser.statement.create.index; exports net.sf.jsqlparser.statement.create.policy; @@ -33,6 +34,8 @@ exports net.sf.jsqlparser.statement.create.sequence; exports net.sf.jsqlparser.statement.create.synonym; exports net.sf.jsqlparser.statement.create.table; + exports net.sf.jsqlparser.statement.create.trigger; + exports net.sf.jsqlparser.statement.create.user; exports net.sf.jsqlparser.statement.create.view; exports net.sf.jsqlparser.statement.delete; exports net.sf.jsqlparser.statement.drop; diff --git a/src/main/java/net/sf/jsqlparser/expression/AliasedExpression.java b/src/main/java/net/sf/jsqlparser/expression/AliasedExpression.java new file mode 100644 index 0000000000..745cdf4259 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/AliasedExpression.java @@ -0,0 +1,59 @@ +/*- + * #%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.function.Consumer; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** An expression with an alias, such as an XMLFOREST argument. */ +public class AliasedExpression extends ASTNodeAccessImpl implements Expression { + private Expression expression; + private Alias alias; + + public AliasedExpression(Expression expression, Alias alias) { + this.expression = expression; + this.alias = alias; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public Alias getAlias() { + return alias; + } + + public void setAlias(Alias alias) { + this.alias = alias; + } + + @Override + public T accept(ExpressionVisitor visitor, S context) { + return visitor.visit(this, context); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + expressionPrinter.accept(expression); + if (alias != null) { + builder.append(alias); + } + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java b/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java index d94d4ef2eb..0f8f56dd65 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.function.Consumer; import java.util.Locale; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; @@ -40,6 +41,7 @@ public class AnalyticExpression extends ASTNodeAccessImpl implements Expression private boolean unique = false; private boolean ignoreNullsOutside = false; // IGNORE NULLS outside function parameters private Expression filterExpression = null; + private boolean filterWhereKeyword = true; private List funcOrderBy = null; private String onOverflowTruncate = null; @@ -58,7 +60,7 @@ public class AnalyticExpression extends ASTNodeAccessImpl implements Expression public AnalyticExpression() {} public AnalyticExpression(Function function) { - this.name = String.join(" ", function.getMultipartName()); + this.name = function.getName(); this.allColumns = function.isAllColumns(); this.distinct = function.isDistinct(); this.unique = function.isUnique(); @@ -338,10 +340,8 @@ public String toString() { b.append(keep).append(" "); } - if (filterExpression != null) { - b.append("FILTER (WHERE "); - b.append(filterExpression); - b.append(")"); + if (filterExpression != null && type != AnalyticType.WITHIN_GROUP) { + appendFilterTo(b, b::append); if (type != AnalyticType.FILTER_ONLY) { b.append(" "); } @@ -382,9 +382,32 @@ public String toString() { b.append(windowDef.toString()); } + if (filterExpression != null && type == AnalyticType.WITHIN_GROUP) { + b.append(' '); + appendFilterTo(b, b::append); + } return b.toString(); } + /** Renders the filter using the caller's expression writer. */ + public void appendFilterTo(StringBuilder builder, Consumer expressionPrinter) { + if (filterExpression != null) { + builder.append(filterWhereKeyword ? "FILTER (WHERE " : "FILTER ("); + expressionPrinter.accept(filterExpression); + builder.append(')'); + } + } + + /** Whether the FILTER clause has WHERE, which DuckDB allows to leave out. */ + public boolean isFilterWhereKeyword() { + return filterWhereKeyword; + } + + public AnalyticExpression setFilterWhereKeyword(boolean filterWhereKeyword) { + this.filterWhereKeyword = filterWhereKeyword; + return this; + } + public boolean isAllColumns() { return allColumns; } diff --git a/src/main/java/net/sf/jsqlparser/expression/ArrayExpression.java b/src/main/java/net/sf/jsqlparser/expression/ArrayExpression.java index e86f34cad8..74072efe34 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ArrayExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/ArrayExpression.java @@ -17,6 +17,7 @@ public class ArrayExpression extends ASTNodeAccessImpl implements Expression { private Expression indexExpression; private Expression startIndexExpression; private Expression stopIndexExpression; + private Expression stepExpression; public ArrayExpression() { @@ -72,6 +73,19 @@ public void setStopIndexExpression(Expression stopIndexExpression) { this.stopIndexExpression = stopIndexExpression; } + public Expression getStepExpression() { + return stepExpression; + } + + public void setStepExpression(Expression stepExpression) { + this.stepExpression = stepExpression; + } + + public ArrayExpression withStepExpression(Expression stepExpression) { + this.setStepExpression(stepExpression); + return this; + } + @Override public T accept(ExpressionVisitor expressionVisitor, S context) { return expressionVisitor.visit(this, context); @@ -82,11 +96,8 @@ public String toString() { if (indexExpression != null) { return objExpression.toString() + "[" + indexExpression.toString() + "]"; } else { - return objExpression.toString() + "[" + - (startIndexExpression == null ? "" : startIndexExpression.toString()) + - ":" + - (stopIndexExpression == null ? "" : stopIndexExpression.toString()) + - "]"; + return objExpression.toString() + "[" + RangeExpression.toString(startIndexExpression, + stopIndexExpression, stepExpression) + "]"; } } diff --git a/src/main/java/net/sf/jsqlparser/expression/CastExpression.java b/src/main/java/net/sf/jsqlparser/expression/CastExpression.java index 66af39ed50..9c77ef825f 100644 --- a/src/main/java/net/sf/jsqlparser/expression/CastExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/CastExpression.java @@ -9,18 +9,17 @@ */ 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; -import net.sf.jsqlparser.statement.select.Select; - import java.util.ArrayList; +import java.util.Locale; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.select.Select; public class CastExpression extends ASTNodeAccessImpl implements Expression { private final static Pattern PATTERN = @@ -204,7 +203,7 @@ public String toString() { if (isImplicitCast) { return colDataType + " " + leftExpression; } else if (keyword != null && !keyword.isEmpty()) { - return columnDefinitions.size() > 1 + return !columnDefinitions.isEmpty() ? keyword + "(" + leftExpression + " AS ROW(" + Select.getStringList(columnDefinitions) + ")" + formatStr + ")" : keyword + "(" + leftExpression + " AS " + colDataType.toString() + formatStr diff --git a/src/main/java/net/sf/jsqlparser/expression/ColumnsExpression.java b/src/main/java/net/sf/jsqlparser/expression/ColumnsExpression.java new file mode 100644 index 0000000000..aa19d555f3 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/ColumnsExpression.java @@ -0,0 +1,84 @@ +/*- + * #%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.ArrayList; +import java.util.List; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * A ClickHouse {@code COLUMNS('regexp')} matcher with one or more transformers, for example + * {@code COLUMNS('^metric_') APPLY(x -> round(x, 2))}. + * + * Without a transformer, {@code COLUMNS('regexp')} keeps parsing as a regular + * {@link net.sf.jsqlparser.expression.Function}. + */ +public class ColumnsExpression extends ASTNodeAccessImpl implements Expression { + + private Expression columns; + private List transformers; + + public ColumnsExpression(Expression columns) { + this.columns = columns; + this.transformers = new ArrayList<>(); + } + + public ColumnsExpression(Expression columns, List transformers) { + this.columns = columns; + this.transformers = transformers; + } + + public Expression getColumns() { + return columns; + } + + public ColumnsExpression setColumns(Expression columns) { + this.columns = columns; + return this; + } + + public List getTransformers() { + return transformers; + } + + public ColumnsExpression setTransformers(List transformers) { + this.transformers = transformers; + return this; + } + + public List getAllExpressions() { + List expressions = new ArrayList<>(); + if (columns != null) { + expressions.add(columns); + } + for (ColumnsTransformer transformer : transformers) { + transformer.collectExpressions(expressions); + } + return expressions; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(columns); + for (ColumnsTransformer transformer : transformers) { + builder.append(" ").append(transformer); + } + return builder; + } + + @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/ColumnsTransformer.java b/src/main/java/net/sf/jsqlparser/expression/ColumnsTransformer.java new file mode 100644 index 0000000000..9f471fe16e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/ColumnsTransformer.java @@ -0,0 +1,213 @@ +/*- + * #%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.List; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectItem; + +/** + * A ClickHouse transformer following a {@code COLUMNS(...)} matcher or a {@code *}, for example the + * {@code APPLY(x -> round(x, 2))} in {@code SELECT COLUMNS('^m') APPLY(x -> round(x, 2))}. + * + * ClickHouse parses its transformers in a loop, so they may repeat and combine in any order. + */ +public class ColumnsTransformer extends ASTNodeAccessImpl { + + public enum ColumnsTransformerType { + APPLY, EXCEPT, EXCLUDE, REPLACE, RENAME + } + + private ColumnsTransformerType type; + private Expression applyExpression; + private ParenthesedExpressionList exceptColumns; + private StringValue exceptPattern; + private List> replaceItems; + private List> renameItems; + private boolean parenthesized = true; + private boolean strict; + + public ColumnsTransformer(ColumnsTransformerType type) { + this.type = type; + } + + public ColumnsTransformerType getType() { + return type; + } + + public ColumnsTransformer setType(ColumnsTransformerType type) { + this.type = type; + return this; + } + + public Expression getApplyExpression() { + return applyExpression; + } + + public ColumnsTransformer setApplyExpression(Expression applyExpression) { + this.applyExpression = applyExpression; + return this; + } + + public ParenthesedExpressionList getExceptColumns() { + return exceptColumns; + } + + public ColumnsTransformer setExceptColumns(ParenthesedExpressionList exceptColumns) { + this.exceptColumns = exceptColumns; + if (exceptColumns != null) { + this.exceptPattern = null; + } + return this; + } + + /** + * The re2 pattern of the ClickHouse {@code EXCEPT 'pattern'} form, mutually exclusive with + * {@link #getExceptColumns()}. + */ + public StringValue getExceptPattern() { + return exceptPattern; + } + + public ColumnsTransformer setExceptPattern(StringValue exceptPattern) { + this.exceptPattern = exceptPattern; + if (exceptPattern != null) { + this.exceptColumns = null; + } + return this; + } + + /** + * The {@code STRICT} modifier of ClickHouse {@code EXCEPT}/{@code REPLACE} transformers: named + * columns must exist, otherwise the database rejects the query. Meaningless for {@code APPLY} + * and {@code EXCLUDE}. + */ + public boolean isStrict() { + return strict; + } + + public ColumnsTransformer setStrict(boolean strict) { + this.strict = strict; + return this; + } + + public List> getReplaceItems() { + return replaceItems; + } + + public ColumnsTransformer setReplaceItems(List> replaceItems) { + this.replaceItems = replaceItems; + return this; + } + + /** DuckDB's {@code RENAME (column AS name, ...)}: each item is a column with its new name. */ + public List> getRenameItems() { + return renameItems; + } + + public ColumnsTransformer setRenameItems(List> renameItems) { + this.renameItems = renameItems; + return this; + } + + /** + * Whether the columns or items of an {@code EXCLUDE}, {@code REPLACE} or {@code RENAME} are + * parenthesized; DuckDB also accepts a single one without parentheses, e.g. + * {@code * EXCLUDE i}. + */ + public boolean isParenthesized() { + return parenthesized; + } + + public ColumnsTransformer setParenthesized(boolean parenthesized) { + this.parenthesized = parenthesized; + return this; + } + + void collectExpressions(List expressions) { + switch (type) { + case APPLY: + if (applyExpression != null) { + expressions.add(applyExpression); + } + break; + case EXCEPT: + if (exceptPattern != null) { + expressions.add(exceptPattern); + } + if (exceptColumns != null) { + expressions.addAll(exceptColumns); + } + break; + case EXCLUDE: + if (exceptColumns != null) { + expressions.addAll(exceptColumns); + } + break; + case REPLACE: + if (replaceItems != null) { + for (SelectItem item : replaceItems) { + expressions.add(item.getExpression()); + } + } + break; + case RENAME: + if (renameItems != null) { + for (SelectItem item : renameItems) { + expressions.add(item.getExpression()); + } + } + break; + default: + throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type); + } + } + + public StringBuilder appendTo(StringBuilder builder) { + switch (type) { + case APPLY: + builder.append("APPLY(").append(applyExpression).append(")"); + break; + case EXCEPT: + builder.append("EXCEPT").append(strict ? " STRICT" : "").append(" ") + .append(exceptPattern != null ? exceptPattern : exceptColumns); + break; + case EXCLUDE: + builder.append("EXCLUDE ").append(parenthesized || exceptColumns.size() != 1 + ? exceptColumns + : exceptColumns.get(0)); + break; + case REPLACE: + boolean replaceParenthesized = parenthesized || replaceItems.size() != 1; + builder.append("REPLACE").append(strict ? " STRICT" : "") + .append(replaceParenthesized ? "(" : " ") + .append(Select.getStringList(replaceItems)) + .append(replaceParenthesized ? ")" : ""); + break; + case RENAME: + boolean renameParenthesized = parenthesized || renameItems.size() != 1; + builder.append("RENAME ").append(renameParenthesized ? "(" : "") + .append(Select.getStringList(renameItems)) + .append(renameParenthesized ? ")" : ""); + break; + default: + throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java b/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java index 922c0c4194..f3c4adfd4b 100644 --- a/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java @@ -18,12 +18,16 @@ public class DateUnitExpression extends ASTNodeAccessImpl implements Expression private final DateUnit type; + private final String text; + public DateUnitExpression(DateUnit type) { this.type = Objects.requireNonNull(type); + this.text = type.toString(); } public DateUnitExpression(String DateUnitStr) { this.type = Objects.requireNonNull(DateUnit.from(DateUnitStr)); + this.text = DateUnitStr; } public DateUnit getType() { @@ -38,11 +42,23 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { - return type.toString(); + return text; } public enum DateUnit { - CENTURY, DECADE, YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND, MILLISECOND, MICROSECOND, NANOSECOND; + CENTURY, DECADE, YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND, MILLISECOND, MICROSECOND, NANOSECOND, SQL_TSI_FRAC_SECOND, SQL_TSI_SECOND, SQL_TSI_MINUTE, SQL_TSI_HOUR, SQL_TSI_DAY, SQL_TSI_WEEK, SQL_TSI_MONTH, SQL_TSI_QUARTER, SQL_TSI_YEAR; + + /** Returns an ODBC interval keyword, or null when the text is not one. */ + public static DateUnit fromOdbcInterval(String text) { + if (text == null || !text.toUpperCase(Locale.ROOT).startsWith("SQL_TSI_")) { + return null; + } + try { + return from(text); + } catch (IllegalArgumentException exception) { + return null; + } + } 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 08afe5129f..e272016245 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import java.util.List; +import net.sf.jsqlparser.statement.execute.ExecuteArgument; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -18,6 +19,7 @@ import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor; import net.sf.jsqlparser.expression.operators.arithmetic.Concat; import net.sf.jsqlparser.expression.operators.arithmetic.Division; +import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation; import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision; import net.sf.jsqlparser.expression.operators.arithmetic.Modulo; import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication; @@ -40,10 +42,12 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; import net.sf.jsqlparser.expression.operators.relational.IsUnknownExpression; +import net.sf.jsqlparser.expression.operators.relational.IsJsonExpression; import net.sf.jsqlparser.expression.operators.relational.JsonOperator; import net.sf.jsqlparser.expression.operators.relational.LikeExpression; import net.sf.jsqlparser.expression.operators.relational.Matches; @@ -70,6 +74,19 @@ import net.sf.jsqlparser.statement.update.UpdateSet; public interface ExpressionVisitor { + /** Visits the value of an aliased expression by default, preserving existing visitors. */ + default T visit(AliasedExpression expression, S context) { + return expression.getExpression().accept(this, context); + } + + default void visit(AliasedExpression expression) { + visit(expression, null); + } + + + default T visit(ExecuteArgument argument, S context) { + return argument.getExpression().accept(this, context); + } default T visitExpressions(ExpressionList expressions, S context) { if (expressions != null) { @@ -255,6 +272,15 @@ default void visit(Division division) { T visit(IntegerDivision integerDivision, S context); + default T visit(Exponentiation exponentiation, S context) { + visitExpression(exponentiation.getLeftExpression(), context); + return visitExpression(exponentiation.getRightExpression(), context); + } + + default void visit(Exponentiation exponentiation) { + this.visit(exponentiation, null); + } + default void visit(IntegerDivision integerDivision) { this.visit(integerDivision, null); } @@ -355,6 +381,14 @@ default void visit(IsBooleanExpression isBooleanExpression) { this.visit(isBooleanExpression, null); } + default T visit(IsJsonExpression expression, S context) { + return expression.getLeftExpression().accept(this, context); + } + + default void visit(IsJsonExpression expression) { + visit(expression, null); + } + T visit(IsUnknownExpression isUnknownExpression, S context); default void visit(IsUnknownExpression isUnknownExpression) { @@ -495,6 +529,14 @@ default void visit(ExtractExpression extractExpression) { this.visit(extractExpression, null); } + default T visit(RowPatternFunction function, S context) { + return function.getFunction().accept(this, context); + } + + default void visit(RowPatternFunction function) { + visit(function, null); + } + T visit(IntervalExpression intervalExpression, S context); default void visit(IntervalExpression intervalExpression) { @@ -563,6 +605,15 @@ default void visit(RowConstructor rowConstructor) { T visit(RowGetExpression rowGetExpression, S context); + default T visit(MethodCallExpression methodCall, S context) { + methodCall.getExpression().accept(this, context); + return methodCall.getMethod().accept(this, context); + } + + default void visit(MethodCallExpression methodCall) { + this.visit(methodCall, null); + } + default void visit(RowGetExpression rowGetExpression) { this.visit(rowGetExpression, null); } @@ -621,6 +672,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) { @@ -717,6 +776,12 @@ default void visit(GeometryDistance geometryDistance) { this.visit(geometryDistance, null); } + T visit(Intersects intersects, S context); + + default void visit(Intersects intersects) { + this.visit(intersects, null); + } + T visit(Select select, S context); T visit(TranscodingFunction transcodingFunction, S context); @@ -737,6 +802,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) { @@ -761,6 +832,29 @@ default void visit(LambdaExpression lambdaExpression) { this.visit(lambdaExpression, null); } + default T visit(ListComprehension listComprehension, S context) { + visitExpression(listComprehension.getExpression(), context); + visitExpression(listComprehension.getList(), context); + return visitExpression(listComprehension.getCondition(), context); + } + + default void visit(ListComprehension listComprehension) { + this.visit(listComprehension, null); + } + + default T visit(ColumnsExpression columnsExpression, S context) { + for (Expression expression : columnsExpression.getAllExpressions()) { + if (expression != null) { + expression.accept(this, context); + } + } + return null; + } + + default void visit(ColumnsExpression columnsExpression) { + this.visit(columnsExpression, null); + } + T visit(HighExpression highExpression, S context); default void visit(HighExpression highExpression) { diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 432c16354d..c5ed899855 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -12,7 +12,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Optional; +import java.util.List; +import java.util.Map; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -21,6 +22,7 @@ import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor; import net.sf.jsqlparser.expression.operators.arithmetic.Concat; import net.sf.jsqlparser.expression.operators.arithmetic.Division; +import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation; import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision; import net.sf.jsqlparser.expression.operators.arithmetic.Modulo; import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication; @@ -43,6 +45,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -111,17 +114,23 @@ public T visit(Function function, S context) { if (function.getParameters() != null) { subExpressions.addAll(function.getParameters()); } + if (function.getNamedParameters() != null) { + subExpressions.addAll(function.getNamedParameters()); + } if (function.getChainedParameters() != null) { subExpressions.addAll(function.getChainedParameters()); } + // an attribute name is not a column reference, only an attribute expression is visited + if (function.getAttribute() instanceof Expression + && function.getAttribute() != function.getAttributeColumn()) { + subExpressions.add((Expression) function.getAttribute()); + } if (function.getKeep() != null) { subExpressions.add(function.getKeep()); } - if (function.getOrderByElements() != null) { - for (OrderByElement orderByElement : function.getOrderByElements()) { - subExpressions.add(orderByElement.getExpression()); - } - } + addOrderByExpressions(subExpressions, function.getOrderByElements()); + addFunctionModifiers(subExpressions, function.getHavingClause(), + function.getKeywordArguments(), function.getLimit()); return visitExpressions(function, context, subExpressions); } @@ -190,6 +199,11 @@ public T visit(IntegerDivision integerDivision, S context) { return visitBinaryExpression(integerDivision, context); } + @Override + public T visit(Exponentiation exponentiation, S context) { + return visitBinaryExpression(exponentiation, context); + } + @Override public T visit(Multiplication multiplication, S context) { return visitBinaryExpression(multiplication, context); @@ -284,7 +298,8 @@ public T visit(IsUnknownExpression isUnknownExpression, S context) { @Override public T visit(LikeExpression likeExpression, S context) { - return visitBinaryExpression(likeExpression, context); + return visitExpressions(likeExpression, context, likeExpression.getLeftExpression(), + likeExpression.getRightExpression(), likeExpression.getEscape()); } @Override @@ -319,7 +334,11 @@ public T visit(ContainedBy containedBy, S context) { @Override public T visit(Column column, S context) { - return applyExpression(column, context); + T result = applyExpression(column, context); + if (column.getArrayConstructor() != null) { + column.getArrayConstructor().accept(this, context); + } + return result; } @Override @@ -358,12 +377,14 @@ public T visit(ExistsExpression existsExpression, S context) { @Override public T visit(MemberOfExpression memberOfExpression, S context) { - return memberOfExpression.getRightExpression().accept(this, context); + return visitExpressions(memberOfExpression, context, + memberOfExpression.getLeftExpression(), memberOfExpression.getRightExpression()); } @Override public T visit(AnyComparisonExpression anyComparisonExpression, S context) { - return applyExpression(anyComparisonExpression, context); + return visitExpressions(anyComparisonExpression, context, + anyComparisonExpression.getSelect()); } @Override @@ -417,29 +438,41 @@ public T visit(AnalyticExpression analyticExpression, S context) { if (analyticExpression.getKeep() != null) { subExpressions.add(analyticExpression.getKeep()); } - if (analyticExpression.getFuncOrderBy() != null) { - for (OrderByElement element : analyticExpression.getOrderByElements()) { - subExpressions.add(element.getExpression()); + subExpressions.add(analyticExpression.getFilterExpression()); + addOrderByExpressions(subExpressions, analyticExpression.getFuncOrderBy()); + addFunctionModifiers(subExpressions, analyticExpression.getHavingClause(), + analyticExpression.getKeywordArguments(), analyticExpression.getLimit()); + if (analyticExpression.getWindowDefinition() != null) { + subExpressions.addAll(analyticExpression.getWindowDefinition().getAllExpressions()); + } + return visitExpressions(analyticExpression, context, subExpressions); + } + + private static void addOrderByExpressions(List expressions, + List orderBy) { + if (orderBy != null) { + for (OrderByElement element : orderBy) { + expressions.add(element.getExpression()); } } - if (analyticExpression.getWindowElement() != null) { - /* - * Visit expressions from the range and offset of the window element. Do this using - * optional chains, because several things down the tree can be null e.g. the - * expression. So, null-safe versions of e.g.: - * analyticExpression.getWindowElement().getOffset().getExpression().accept(this, - * parameters); - */ - Optional.ofNullable(analyticExpression.getWindowElement().getRange()) - .map(WindowRange::getStart) - .map(WindowOffset::getExpression).ifPresent(subExpressions::add); - Optional.ofNullable(analyticExpression.getWindowElement().getRange()) - .map(WindowRange::getEnd) - .map(WindowOffset::getExpression).ifPresent(subExpressions::add); - Optional.ofNullable(analyticExpression.getWindowElement().getOffset()) - .map(WindowOffset::getExpression).ifPresent(subExpressions::add); + } + + private static void addFunctionModifiers(List expressions, + Function.HavingClause having, List arguments, + net.sf.jsqlparser.statement.select.Limit limit) { + expressions.add(having); + if (arguments != null) { + for (Function.KeywordArgument argument : arguments) { + expressions.add(argument.getExpression()); + } + } + if (limit != null) { + expressions.add(limit.getOffset()); + expressions.add(limit.getRowCount()); + if (limit.getByExpressions() != null) { + expressions.addAll(limit.getByExpressions()); + } } - return visitExpressions(analyticExpression, context, subExpressions); } @Override @@ -523,7 +556,10 @@ protected T visitBinaryExpression(BinaryExpression binaryExpression, S conte @Override public T visit(JsonExpression jsonExpr, S context) { - return jsonExpr.getExpression().accept(this, context); + ArrayList subExpressions = new ArrayList<>(); + subExpressions.add(jsonExpr.getExpression()); + subExpressions.addAll(jsonExpr.getIdents()); + return visitExpressions(jsonExpr, context, subExpressions); } @Override @@ -685,6 +721,9 @@ public T visit(ArrayExpression arrayExpression, S context) { if (arrayExpression.getStopIndexExpression() != null) { subExpressions.add(arrayExpression.getStopIndexExpression()); } + if (arrayExpression.getStepExpression() != null) { + subExpressions.add(arrayExpression.getStepExpression()); + } return visitExpressions(arrayExpression, context, subExpressions); } @@ -693,6 +732,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(), @@ -701,24 +750,33 @@ public T visit(VariableAssignment variableAssignment, S context) { @Override public T visit(XMLSerializeExpr xmlSerializeExpr, S context) { - ArrayList subExpressions = new ArrayList<>(); - - subExpressions.add(xmlSerializeExpr.getExpression()); - for (OrderByElement orderByElement : xmlSerializeExpr.getOrderByElements()) { - subExpressions.add(orderByElement.getExpression()); - } - return visitExpressions(xmlSerializeExpr, context, subExpressions); + return visitExpressions(xmlSerializeExpr, context, xmlSerializeExpr.getExpressions()); } @Override public T visit(TimezoneExpression timezoneExpression, S context) { - return timezoneExpression.getLeftExpression().accept(this, context); + ArrayList subExpressions = new ArrayList<>(); + subExpressions.add(timezoneExpression.getLeftExpression()); + subExpressions.addAll(timezoneExpression.getTimezoneExpressions()); + return visitExpressions(timezoneExpression, context, subExpressions); } @Override public T visit(JsonAggregateFunction jsonAggregateFunction, S context) { - return visitExpressions(jsonAggregateFunction, context, - jsonAggregateFunction.getExpression(), jsonAggregateFunction.getFilterExpression()); + ArrayList subExpressions = new ArrayList<>(); + subExpressions.add(jsonAggregateFunction.getExpression()); + if (jsonAggregateFunction.getKey() instanceof Expression) { + subExpressions.add((Expression) jsonAggregateFunction.getKey()); + } + if (jsonAggregateFunction.getValue() instanceof Expression) { + subExpressions.add((Expression) jsonAggregateFunction.getValue()); + } + addOrderByExpressions(subExpressions, + jsonAggregateFunction.getExpressionOrderByElements()); + subExpressions.add(jsonAggregateFunction.getFilterExpression()); + subExpressions.addAll(jsonAggregateFunction.getPartitionExpressionList()); + addOrderByExpressions(subExpressions, jsonAggregateFunction.getOrderByElements()); + return visitExpressions(jsonAggregateFunction, context, subExpressions); } @Override @@ -793,6 +851,11 @@ public T visit(GeometryDistance geometryDistance, S context) { return visitBinaryExpression(geometryDistance, context); } + @Override + public T visit(Intersects intersects, S context) { + return visitBinaryExpression(intersects, context); + } + @Override public T visit(Select select, S context) { if (selectVisitor != null) { @@ -813,13 +876,20 @@ public T visit(TranscodingFunction transcodingFunction, S context) { @Override public T visit(TrimFunction trimFunction, S context) { - return trimFunction.getExpression().accept(this, context); + return visitExpressions(trimFunction, context, trimFunction.getExpression(), + trimFunction.getFromExpression()); } @Override public T visit(RangeExpression rangeExpression, S context) { return visitExpressions(rangeExpression, context, rangeExpression.getStartExpression(), - rangeExpression.getEndExpression()); + rangeExpression.getEndExpression(), rangeExpression.getStepExpression()); + } + + @Override + public T visit(TernaryExpression ternaryExpression, S context) { + return visitExpressions(ternaryExpression, context, ternaryExpression.getCondition(), + ternaryExpression.getThenExpression(), ternaryExpression.getElseExpression()); } @Override @@ -848,6 +918,17 @@ public T visit(LambdaExpression lambdaExpression, S context) { return lambdaExpression.getExpression().accept(this, context); } + @Override + public T visit(ListComprehension listComprehension, S context) { + return visitExpressions(listComprehension, context, listComprehension.getExpression(), + listComprehension.getList(), listComprehension.getCondition()); + } + + @Override + public T visit(ColumnsExpression columnsExpression, S context) { + return visitExpressions(columnsExpression, context, columnsExpression.getAllExpressions()); + } + @Override public T visit(HighExpression highExpression, S context) { return highExpression.getExpression().accept(this, context); diff --git a/src/main/java/net/sf/jsqlparser/expression/Function.java b/src/main/java/net/sf/jsqlparser/expression/Function.java index 581b13cf50..e411bab60f 100644 --- a/src/main/java/net/sf/jsqlparser/expression/Function.java +++ b/src/main/java/net/sf/jsqlparser/expression/Function.java @@ -17,6 +17,9 @@ import net.sf.jsqlparser.statement.select.Limit; import net.sf.jsqlparser.statement.select.OrderByElement; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.select.PlainSelect; + import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; @@ -46,6 +49,7 @@ public class Function extends ASTNodeAccessImpl implements Expression { private KeepExpression keep = null; private String onOverflowTruncate = null; private String extraKeyword = null; + private List resultColumnDefinitions; /** * Generic keyword arguments captured inside function parentheses, e.g. @@ -55,6 +59,23 @@ public class Function extends ASTNodeAccessImpl implements Expression { */ private List keywordArguments = null; + /** Column definitions supplied for a record-returning function inside ROWS FROM. */ + public List getResultColumnDefinitions() { + return resultColumnDefinitions; + } + + public void setResultColumnDefinitions(List resultColumnDefinitions) { + this.resultColumnDefinitions = resultColumnDefinitions; + } + + public StringBuilder appendResultColumnDefinitionsTo(StringBuilder builder) { + if (resultColumnDefinitions != null) { + builder.append(" AS ") + .append(PlainSelect.getStringList(resultColumnDefinitions, true, true)); + } + return builder; + } + public Function() {} public Function(String name, Expression... parameters) { @@ -446,7 +467,8 @@ public String toString() { ans = "{fn " + ans + "}"; } - return ans; + return resultColumnDefinitions == null ? ans + : appendResultColumnDefinitionsTo(new StringBuilder(ans)).toString(); } public Function withAttribute(Expression attribute) { diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java b/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java index f258e855cb..7cd34b53d7 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 dfad9451a0..7331ded7d3 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Locale; import java.util.Objects; +import java.util.function.Consumer; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.create.table.ColDataType; @@ -77,6 +78,11 @@ public void setExpression(Expression expression) { } public StringBuilder append(StringBuilder builder) { + return appendTo(builder, builder::append); + } + + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { switch (type) { case ERROR: builder.append("ERROR"); @@ -85,7 +91,12 @@ public StringBuilder append(StringBuilder builder) { builder.append("NULL"); break; case DEFAULT: - builder.append("DEFAULT ").append(expression); + builder.append("DEFAULT "); + if (expression == null) { + builder.append("null"); + } else { + expressionPrinter.accept(expression); + } break; case EMPTY: builder.append("EMPTY "); @@ -389,23 +400,28 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { // avoid countless Builder --> String conversion public StringBuilder append(StringBuilder builder) { + return appendTo(builder, builder::append); + } + + /** Renders structured children through the supplied expression printer. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { switch (functionType) { case OBJECT: case POSTGRES_OBJECT: case MYSQL_OBJECT: - appendObject(builder); + appendObject(builder, expressionPrinter); break; case ARRAY: - appendArray(builder); + appendArray(builder, expressionPrinter); break; case VALUE: - appendValue(builder); + appendValue(builder, expressionPrinter); break; case QUERY: - appendQuery(builder); + appendQuery(builder, expressionPrinter); break; case EXISTS: - appendExists(builder); + appendExists(builder, expressionPrinter); break; default: // this should never happen really @@ -413,15 +429,19 @@ public StringBuilder append(StringBuilder builder) { return builder; } - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder appendObject(StringBuilder builder) { - builder.append("JSON_OBJECT( "); + return appendObject(builder, builder::append); + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendObject(StringBuilder builder, + Consumer expressionPrinter) { + builder.append("JSON_OBJECT("); + int start = builder.length(); int i = 0; for (JsonKeyValuePair keyValuePair : keyValuePairs) { - if (i > 0) { - builder.append(", "); - } - keyValuePair.append(builder); + builder.append(i > 0 ? ", " : " "); + keyValuePair.appendTo(builder, expressionPrinter); i++; } @@ -432,7 +452,8 @@ public StringBuilder appendObject(StringBuilder builder) { appendUniqueKeys(builder); appendReturningClause(builder, true); - builder.append(" ) "); + // An empty argument list renders as JSON_OBJECT() + builder.append(builder.length() > start ? " )" : ")"); return builder; } @@ -467,53 +488,66 @@ private void appendUniqueKeys(StringBuilder builder) { } } - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder appendArray(StringBuilder builder) { - builder.append("JSON_ARRAY( "); + return appendArray(builder, builder::append); + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendArray(StringBuilder builder, + Consumer expressionPrinter) { + builder.append("JSON_ARRAY("); int i = 0; for (JsonFunctionExpression expr : expressions) { - if (i > 0) { - builder.append(", "); - } - expr.append(builder); + builder.append(i > 0 ? ", " : " "); + expr.appendTo(builder, expressionPrinter); i++; } appendOnNullType(builder); appendReturningClause(builder, true); - builder.append(") "); + builder.append(')'); return builder; } - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder appendValue(StringBuilder builder) { + return appendValue(builder, builder::append); + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendValue(StringBuilder builder, + Consumer expressionPrinter) { builder.append("JSON_VALUE("); - appendValueOrQueryPrefix(builder); + appendValueOrQueryPrefix(builder, expressionPrinter); if (returningType != null) { builder.append(" RETURNING ").append(returningType); } - appendOnResponseClause(builder, onEmptyBehavior, "EMPTY"); - appendOnResponseClause(builder, onErrorBehavior, "ERROR"); + appendOnResponseClause(builder, onEmptyBehavior, "EMPTY", expressionPrinter); + appendOnResponseClause(builder, onErrorBehavior, "ERROR", expressionPrinter); builder.append(")"); return builder; } - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder appendQuery(StringBuilder builder) { + return appendQuery(builder, builder::append); + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendQuery(StringBuilder builder, + Consumer expressionPrinter) { builder.append("JSON_QUERY("); - appendValueOrQueryPrefix(builder); + appendValueOrQueryPrefix(builder, expressionPrinter); appendReturningClause(builder, true); appendWrapperClause(builder); appendQuotesClause(builder); - appendOnResponseClause(builder, onEmptyBehavior, "EMPTY"); - appendOnResponseClause(builder, onErrorBehavior, "ERROR"); + appendOnResponseClause(builder, onEmptyBehavior, "EMPTY", expressionPrinter); + appendOnResponseClause(builder, onErrorBehavior, "ERROR", expressionPrinter); for (String additionalQueryPathArgument : additionalQueryPathArguments) { builder.append(", ").append(additionalQueryPathArgument); @@ -523,25 +557,31 @@ public StringBuilder appendQuery(StringBuilder builder) { return builder; } - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder appendExists(StringBuilder builder) { + return appendExists(builder, builder::append); + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendExists(StringBuilder builder, + Consumer expressionPrinter) { builder.append("JSON_EXISTS("); - appendValueOrQueryPrefix(builder); - appendOnResponseClause(builder, onErrorBehavior, "ERROR"); + appendValueOrQueryPrefix(builder, expressionPrinter); + appendOnResponseClause(builder, onErrorBehavior, "ERROR", expressionPrinter); builder.append(")"); return builder; } - private void appendValueOrQueryPrefix(StringBuilder builder) { + private void appendValueOrQueryPrefix(StringBuilder builder, + Consumer expressionPrinter) { if (inputExpression != null) { - inputExpression.append(builder); + inputExpression.appendTo(builder, expressionPrinter); } if (jsonPathExpression != null) { if (inputExpression != null) { builder.append(", "); } - builder.append(jsonPathExpression); + expressionPrinter.accept(jsonPathExpression); } if (!passingExpressions.isEmpty()) { @@ -553,16 +593,20 @@ private void appendValueOrQueryPrefix(StringBuilder builder) { } else { comma = true; } - builder.append(passingExpression); + if (passingExpression == null) { + builder.append("null"); + } else { + expressionPrinter.accept(passingExpression); + } } } } private void appendOnResponseClause(StringBuilder builder, JsonOnResponseBehavior behavior, - String clause) { + String clause, Consumer expressionPrinter) { if (behavior != null) { builder.append(" "); - behavior.append(builder); + behavior.appendTo(builder, expressionPrinter); builder.append(" ON ").append(clause); } } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java index 738c09fc2d..b8a7007f5b 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java @@ -12,6 +12,7 @@ import java.io.Serializable; import java.util.Objects; +import java.util.function.Consumer; /** * @author Andreas Reichel @@ -58,7 +59,11 @@ public JsonFunctionExpression withEncoding(String encoding) { } public StringBuilder append(StringBuilder builder) { - builder.append(getExpression()); + return appendTo(builder, builder::append); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + expressionPrinter.accept(getExpression()); if (isUsingFormatJson()) { builder.append(" FORMAT JSON"); if (encoding != null) { diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java index 18fb4752d9..bd0ed4dad4 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java @@ -12,6 +12,7 @@ import java.io.Serializable; import java.util.Objects; +import java.util.function.Consumer; /** * @author Andreas Reichel @@ -153,14 +154,18 @@ public Object getValue() { } public StringBuilder append(StringBuilder builder) { + return appendTo(builder, builder::append); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { if (isUsingKeyKeyword() && getSeparator() == JsonKeyValuePairSeparator.VALUE) { builder.append("KEY "); } - builder.append(getKey()); + appendValue(builder, getKey(), expressionPrinter); if (getValue() != null) { builder.append(getSeparator().getSeparatorString()); - builder.append(getValue()); + appendValue(builder, getValue(), expressionPrinter); } if (isUsingFormatJson()) { @@ -173,6 +178,15 @@ public StringBuilder append(StringBuilder builder) { return builder; } + private static void appendValue(StringBuilder builder, Object value, + Consumer expressionPrinter) { + if (value instanceof Expression) { + expressionPrinter.accept((Expression) value); + } else { + builder.append(value); + } + } + @Override public String toString() { return append(new StringBuilder()).toString(); diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java index 5ee166e6fc..65bb76f5fb 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.create.table.ColDataType; @@ -93,7 +94,14 @@ public void collectExpressions(List expressions) { @Override public String toString() { - return valueExpression + " AS " + parameterName; + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { + printExpression(builder, valueExpression, expressionPrinter); + return builder.append(" AS ").append(parameterName); } } @@ -305,13 +313,22 @@ public void collectExpressions(List expressions) { @Override public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { if (nestedPlanExpression != null) { - return "(" + nestedPlanExpression + ")"; - } - if (name != null) { - return name; + builder.append("("); + nestedPlanExpression.appendTo(builder, expressionPrinter); + builder.append(")"); + } else if (name != null) { + builder.append(name); + } else if (expression != null) { + expressionPrinter.accept(expression); } - return expression != null ? expression.toString() : ""; + return builder; } } @@ -348,13 +365,28 @@ public void collectExpressions(List expressions) { @Override public String toString() { StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { if (!terms.isEmpty()) { - builder.append(terms.get(0)); + appendTerm(builder, terms.get(0), expressionPrinter); } for (int i = 0; i < operators.size() && i + 1 < terms.size(); i++) { - builder.append(operators.get(i).getDisplay()).append(terms.get(i + 1)); + builder.append(operators.get(i).getDisplay()); + appendTerm(builder, terms.get(i + 1), expressionPrinter); + } + return builder; + } + + private static void appendTerm(StringBuilder builder, JsonTablePlanTerm term, + Consumer expressionPrinter) { + if (term == null) { + builder.append("null"); + } else { + term.appendTo(builder, expressionPrinter); } - return builder.toString(); } } @@ -388,18 +420,36 @@ public void collectExpressions(List expressions) { @Override public String toString() { - StringBuilder builder = new StringBuilder("PLAN"); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { + builder.append("PLAN"); if (defaultPlan) { builder.append(" DEFAULT"); } - builder.append(" (").append(planExpression).append(")"); - return builder.toString(); + builder.append(" ("); + if (planExpression == null) { + builder.append("null"); + } else { + planExpression.appendTo(builder, expressionPrinter); + } + builder.append(")"); + return builder; } } public abstract static class JsonTableColumnDefinition extends ASTNodeAccessImpl implements Serializable { public abstract void collectExpressions(List expressions); + + /** Custom column definitions retain their existing SQL renderer by default. */ + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { + return builder.append(this); + } } public static class JsonTableNestedColumnDefinition extends JsonTableColumnDefinition { @@ -457,16 +507,28 @@ public void collectExpressions(List expressions) { @Override public String toString() { - StringBuilder builder = new StringBuilder("NESTED"); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { + builder.append("NESTED"); if (pathKeyword) { builder.append(" PATH"); } - builder.append(" ").append(pathExpression); + builder.append(" "); + printExpression(builder, pathExpression, expressionPrinter); if (pathName != null) { builder.append(" AS ").append(pathName); } - builder.append(" ").append(columnsClause); - return builder.toString(); + builder.append(" "); + if (columnsClause == null) { + builder.append("null"); + } else { + columnsClause.appendTo(builder, expressionPrinter); + } + return builder; } } @@ -474,6 +536,7 @@ public static class JsonTableValueColumnDefinition extends JsonTableColumnDefini private String columnName; private boolean forOrdinality; private ColDataType dataType; + private String collation; private boolean formatJson; private boolean exists; private boolean onEmptyAfterOnError; @@ -526,6 +589,16 @@ public JsonTableValueColumnDefinition setDataType(ColDataType dataType) { return this; } + /** Returns the column collation, including any identifier quoting, or null if absent. */ + public String getCollation() { + return collation; + } + + public JsonTableValueColumnDefinition setCollation(String collation) { + this.collation = collation; + return this; + } + public boolean isFormatJson() { return formatJson; } @@ -615,17 +688,44 @@ public void collectExpressions(List expressions) { @Override public String toString() { - StringBuilder builder = new StringBuilder(columnName); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { + builder.append(columnName); if (forOrdinality) { builder.append(" FOR ORDINALITY"); - return builder.toString(); + return builder; } - if (exists) { - builder.append(" EXISTS"); + appendTypeOptions(builder); + appendPathOptions(builder, expressionPrinter); + if (onEmptyBehavior != null && !onEmptyAfterOnError) { + builder.append(" "); + onEmptyBehavior.appendTo(builder, expressionPrinter); + builder.append(" ON EMPTY"); + } + if (onErrorBehavior != null) { + builder.append(" "); + onErrorBehavior.appendTo(builder, expressionPrinter); + builder.append(" ON ERROR"); } + if (onEmptyBehavior != null && onEmptyAfterOnError) { + builder.append(" "); + onEmptyBehavior.appendTo(builder, expressionPrinter); + builder.append(" ON EMPTY"); + } + return builder; + } + + private void appendTypeOptions(StringBuilder builder) { if (dataType != null) { builder.append(" ").append(dataType); } + if (collation != null) { + builder.append(" COLLATE ").append(collation); + } if (formatJson) { builder.append(" FORMAT JSON"); if (encoding != null) { @@ -637,11 +737,19 @@ public String toString() { builder.append(scalarsType); builder.append(" SCALARS"); } + if (exists) { + builder.append(" EXISTS"); + } + } + + private void appendPathOptions(StringBuilder builder, + Consumer expressionPrinter) { if (wrapperClause != null && wrapperClause.isBeforePathExpression()) { builder.append(" ").append(wrapperClause); } if (pathExpression != null) { - builder.append(" PATH ").append(pathExpression); + builder.append(" PATH "); + expressionPrinter.accept(pathExpression); } if (wrapperClause != null && !wrapperClause.isBeforePathExpression()) { builder.append(" ").append(wrapperClause); @@ -649,16 +757,6 @@ public String toString() { 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(); } } @@ -685,17 +783,27 @@ public void collectExpressions(List expressions) { @Override public String toString() { - StringBuilder builder = new StringBuilder("COLUMNS ("); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { + builder.append("COLUMNS ("); boolean first = true; for (JsonTableColumnDefinition columnDefinition : columnDefinitions) { if (!first) { builder.append(", "); } - builder.append(columnDefinition); + if (columnDefinition == null) { + builder.append("null"); + } else { + columnDefinition.appendTo(builder, expressionPrinter); + } first = false; } builder.append(")"); - return builder.toString(); + return builder; } } @@ -820,17 +928,57 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { - StringBuilder builder = new StringBuilder("JSON_TABLE("); - builder.append(jsonInputExpression); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append("JSON_TABLE("); + appendInputAndPath(builder, expressionPrinter); + appendPassingClauses(builder, expressionPrinter); + 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(" "); + if (columnsClause == null) { + builder.append("null"); + } else { + columnsClause.appendTo(builder, expressionPrinter); + } + if (planClause != null) { + builder.append(" "); + planClause.appendTo(builder, expressionPrinter); + } + if (onErrorClause != null && !onErrorClause.isBeforeColumns()) { + builder.append(" ").append(onErrorClause); + } + builder.append(")"); + return builder; + } + + private void appendInputAndPath(StringBuilder builder, + Consumer expressionPrinter) { + printExpression(builder, jsonInputExpression, expressionPrinter); if (formatJson) { builder.append(" FORMAT JSON"); } if (jsonPathExpression != null) { - builder.append(", ").append(jsonPathExpression); + builder.append(", "); + expressionPrinter.accept(jsonPathExpression); } if (pathName != null) { builder.append(" AS ").append(pathName); } + } + + private void appendPassingClauses(StringBuilder builder, + Consumer expressionPrinter) { if (!passingClauses.isEmpty()) { builder.append(" PASSING "); boolean first = true; @@ -838,27 +986,23 @@ public String toString() { if (!first) { builder.append(", "); } - builder.append(passingClause); + if (passingClause == null) { + builder.append("null"); + } else { + passingClause.appendTo(builder, expressionPrinter); + } 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); + } + + private static void printExpression(StringBuilder builder, Expression expression, + Consumer expressionPrinter) { + if (expression == null) { + builder.append("null"); + } else { + expressionPrinter.accept(expression); } - builder.append(")"); - return builder.toString(); } + } diff --git a/src/main/java/net/sf/jsqlparser/expression/LambdaExpression.java b/src/main/java/net/sf/jsqlparser/expression/LambdaExpression.java index e2819060f5..2b992173f2 100644 --- a/src/main/java/net/sf/jsqlparser/expression/LambdaExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/LambdaExpression.java @@ -10,15 +10,19 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; public class LambdaExpression extends ASTNodeAccessImpl implements Expression { private List identifiers; private Expression expression; + private boolean parenthesized; + private boolean lambdaKeyword; public LambdaExpression(String identifier, Expression expression) { this.identifiers = Collections.singletonList(identifier); @@ -36,7 +40,28 @@ public static LambdaExpression from(ExpressionList express for (Expression variable : expressionList) { identifiers.add(variable.toString()); } - return new LambdaExpression(identifiers, expression); + return new LambdaExpression(identifiers, expression) + .setParenthesized(expressionList instanceof ParenthesedExpressionList); + } + + /** Whether the parameter list was explicitly parenthesized, including a single parameter. */ + public boolean isParenthesized() { + return parenthesized; + } + + public LambdaExpression setParenthesized(boolean parenthesized) { + this.parenthesized = parenthesized; + return this; + } + + /** Whether it was written with DuckDB's lambda keyword: {@code lambda x, y : expr}. */ + public boolean isLambdaKeyword() { + return lambdaKeyword; + } + + public LambdaExpression setLambdaKeyword(boolean lambdaKeyword) { + this.lambdaKeyword = lambdaKeyword; + return this; } public List getIdentifiers() { @@ -58,7 +83,16 @@ public LambdaExpression setExpression(Expression expression) { } public StringBuilder appendTo(StringBuilder builder) { - if (identifiers.size() == 1) { + return appendTo(builder, builder::append); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + if (lambdaKeyword) { + builder.append("lambda ").append(String.join(", ", identifiers)).append(" : "); + expressionPrinter.accept(expression); + return builder; + } + if (identifiers.size() == 1 && !parenthesized) { builder.append(identifiers.get(0)); } else { int i = 0; @@ -68,7 +102,9 @@ public StringBuilder appendTo(StringBuilder builder) { } builder.append(" )"); } - return builder.append(" -> ").append(expression); + builder.append(" -> "); + expressionPrinter.accept(expression); + return builder; } @Override diff --git a/src/main/java/net/sf/jsqlparser/expression/ListComprehension.java b/src/main/java/net/sf/jsqlparser/expression/ListComprehension.java new file mode 100644 index 0000000000..e65fab8853 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/ListComprehension.java @@ -0,0 +1,98 @@ +/*- + * #%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 net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +import java.util.List; +import java.util.function.Consumer; + +/** + * DuckDB's list comprehension {@code [expression FOR x[, i] IN list [IF condition]]}, where + * {@code i} is the 1-based index of {@code x}. + * + * @see List + * comprehension + */ +public class ListComprehension extends ASTNodeAccessImpl implements Expression { + private Expression expression; + private List variables; + private Expression list; + private Expression condition; + + public ListComprehension(Expression expression, List variables, Expression list, + Expression condition) { + this.expression = expression; + this.variables = variables; + this.list = list; + this.condition = condition; + } + + public Expression getExpression() { + return expression; + } + + public ListComprehension setExpression(Expression expression) { + this.expression = expression; + return this; + } + + /** The element variable, followed by the index variable if any. */ + public List getVariables() { + return variables; + } + + public ListComprehension setVariables(List variables) { + this.variables = variables; + return this; + } + + public Expression getList() { + return list; + } + + public ListComprehension setList(Expression list) { + this.list = list; + return this; + } + + /** The {@code IF} filter, or {@code null}. */ + public Expression getCondition() { + return condition; + } + + public ListComprehension setCondition(Expression condition) { + this.condition = condition; + return this; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) { + builder.append("["); + expressionRenderer.accept(expression); + builder.append(" FOR ").append(String.join(", ", variables)).append(" IN "); + expressionRenderer.accept(list); + if (condition != null) { + builder.append(" IF "); + expressionRenderer.accept(condition); + } + return builder.append("]"); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } +} 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 0000000000..1e64a7db84 --- /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/MethodCallExpression.java b/src/main/java/net/sf/jsqlparser/expression/MethodCallExpression.java new file mode 100644 index 0000000000..30fb798891 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/MethodCallExpression.java @@ -0,0 +1,58 @@ +/*- + * #%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.function.Consumer; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** A method applied to an expression, such as a SQL Server XML subquery result. */ +public class MethodCallExpression extends ASTNodeAccessImpl implements Expression { + private Expression expression; + private Function method; + + public MethodCallExpression(Expression expression, Function method) { + this.expression = expression; + this.method = method; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public Function getMethod() { + return method; + } + + public void setMethod(Function method) { + this.method = method; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + printer.accept(expression); + builder.append('.'); + printer.accept(method); + return builder; + } + + @Override + public T accept(ExpressionVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, value -> builder.append(value)).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/OracleHint.java b/src/main/java/net/sf/jsqlparser/expression/OracleHint.java index 4ab164f987..166e19e08d 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/RangeExpression.java b/src/main/java/net/sf/jsqlparser/expression/RangeExpression.java index dd05827d64..326f5b7da5 100644 --- a/src/main/java/net/sf/jsqlparser/expression/RangeExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/RangeExpression.java @@ -14,6 +14,7 @@ public class RangeExpression extends ASTNodeAccessImpl implements Expression { private Expression startExpression; private Expression endExpression; + private Expression stepExpression; public RangeExpression(Expression startExpression, Expression endExpression) { this.startExpression = startExpression; @@ -38,9 +39,42 @@ public RangeExpression setEndExpression(Expression endExpression) { return this; } + public Expression getStepExpression() { + return stepExpression; + } + + public RangeExpression setStepExpression(Expression stepExpression) { + this.stepExpression = stepExpression; + return this; + } + + public RangeExpression withStepExpression(Expression stepExpression) { + return setStepExpression(stepExpression); + } + + /** + * Renders {@code [start]:[end][:step]}, a missing end before a step as DuckDB's {@code -}. + */ + public static String toString(Expression start, Expression end, Expression step) { + StringBuilder builder = new StringBuilder(); + if (start != null) { + builder.append(start); + } + builder.append(":"); + if (end != null) { + builder.append(end); + } else if (step != null) { + builder.append("-"); + } + if (step != null) { + builder.append(":").append(step); + } + return builder.toString(); + } + @Override public String toString() { - return startExpression + ":" + endExpression; + return toString(startExpression, endExpression, stepExpression); } @Override diff --git a/src/main/java/net/sf/jsqlparser/expression/RowPatternFunction.java b/src/main/java/net/sf/jsqlparser/expression/RowPatternFunction.java new file mode 100644 index 0000000000..a6ee2cd986 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/RowPatternFunction.java @@ -0,0 +1,63 @@ +/*- + * #%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.function.Consumer; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** An explicit RUNNING or FINAL evaluation mode on a row-pattern function. */ +public class RowPatternFunction extends ASTNodeAccessImpl implements Expression { + public enum EvaluationMode { + RUNNING, FINAL + } + + private EvaluationMode evaluationMode; + private Function function; + + public RowPatternFunction(EvaluationMode evaluationMode, Function function) { + this.evaluationMode = evaluationMode; + this.function = function; + } + + public EvaluationMode getEvaluationMode() { + return evaluationMode; + } + + public RowPatternFunction setEvaluationMode(EvaluationMode mode) { + evaluationMode = mode; + return this; + } + + public Function getFunction() { + return function; + } + + public RowPatternFunction setFunction(Function function) { + this.function = function; + return this; + } + + @Override + public T accept(ExpressionVisitor visitor, S context) { + return visitor.visit(this, context); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + builder.append(evaluationMode).append(' '); + expressions.accept(function); + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, expression -> builder.append(expression)).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/SQLServerHints.java b/src/main/java/net/sf/jsqlparser/expression/SQLServerHints.java index 34d14964be..1d82304043 100644 --- a/src/main/java/net/sf/jsqlparser/expression/SQLServerHints.java +++ b/src/main/java/net/sf/jsqlparser/expression/SQLServerHints.java @@ -17,6 +17,18 @@ public class SQLServerHints implements Serializable { private Boolean noLock; private String indexName; + private final List lockHints = new ArrayList<>(); + + /** Additional argument-free locking hints; NOLOCK retains its existing accessors. */ + public enum LockHint { + HOLDLOCK, NOWAIT, PAGLOCK, READCOMMITTED, READCOMMITTEDLOCK, READPAST, READUNCOMMITTED, REPEATABLEREAD, ROWLOCK, SERIALIZABLE, SNAPSHOT, TABLOCK, TABLOCKX, UPDLOCK, XLOCK + } + + /** Returns the mutable list of locking hints, in their original order. */ + public List getLockHints() { + return lockHints; + } + public SQLServerHints() {} @@ -50,6 +62,9 @@ public String toString() { if (Boolean.TRUE.equals(noLock)) { hints.add("NOLOCK"); } + for (LockHint hint : lockHints) { + hints.add(hint.name()); + } return " WITH (" + String.join(", ", hints) + ")"; diff --git a/src/main/java/net/sf/jsqlparser/expression/SignedExpression.java b/src/main/java/net/sf/jsqlparser/expression/SignedExpression.java index 725d449e89..43c92e29b3 100644 --- a/src/main/java/net/sf/jsqlparser/expression/SignedExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/SignedExpression.java @@ -54,7 +54,23 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { - return getSign() + expression.toString(); + String operand = expression.toString(); + return getSign() + (needsSpaceBefore(operand) ? " " : "") + operand; + } + + /** + * Whether the sign must be separated from an operand starting with {@code firstChar}: "- -1" + * must not become the line comment "--1", nor "~ -a" or "~ ~a" a single operator such as "~-" + * or the LIKE operator "~~", as DuckDB and PostgreSQL read a run of signs containing {@code ~}. + */ + public boolean needsSpaceBefore(char firstChar) { + boolean signedOperand = firstChar == '+' || firstChar == '-' || firstChar == '~'; + return sign == '-' && firstChar == '-' || sign == '~' && signedOperand + || firstChar == '~' && (sign == '+' || sign == '-'); + } + + private boolean needsSpaceBefore(String operand) { + return !operand.isEmpty() && needsSpaceBefore(operand.charAt(0)); } public SignedExpression withSign(char sign) { diff --git a/src/main/java/net/sf/jsqlparser/expression/SpannerInterleaveIn.java b/src/main/java/net/sf/jsqlparser/expression/SpannerInterleaveIn.java index 4dcb0aa7f5..dc5e56d208 100644 --- a/src/main/java/net/sf/jsqlparser/expression/SpannerInterleaveIn.java +++ b/src/main/java/net/sf/jsqlparser/expression/SpannerInterleaveIn.java @@ -9,13 +9,13 @@ */ package net.sf.jsqlparser.expression; -import java.util.Locale; -import net.sf.jsqlparser.schema.Table; - +import java.io.Serializable; import java.util.Collections; import java.util.List; +import java.util.Locale; +import net.sf.jsqlparser.schema.Table; -public class SpannerInterleaveIn { +public class SpannerInterleaveIn implements Serializable { private Table table; private OnDelete onDelete; diff --git a/src/main/java/net/sf/jsqlparser/expression/StringValue.java b/src/main/java/net/sf/jsqlparser/expression/StringValue.java index a16536fab5..54a4f7b463 100644 --- a/src/main/java/net/sf/jsqlparser/expression/StringValue.java +++ b/src/main/java/net/sf/jsqlparser/expression/StringValue.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.expression; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -22,9 +24,35 @@ public final class StringValue extends ASTNodeAccessImpl implements Expression { public static final List ALLOWED_PREFIXES = Arrays.asList("N", "U", "E", "R", "B", "RB", "_utf8", "Q"); + + /** The prefix of a PostgreSQL Unicode escape string, as in {@code U&'\0041'}. */ + private static final String UNICODE_ESCAPE_PREFIX = "U&"; + + /** + * How a backslash in the literal body was read. The parser records the escaping of the + * configuration that lexed the literal; constructed values default to {@link #STANDARD}. + */ + public enum Escaping { + /** Only a doubled quote character is an escape, as in standard SQL. */ + STANDARD, + /** MySQL/MariaDB backslash escapes, for example {@code \n}, {@code \'} and {@code \\}. */ + MYSQL_BACKSLASH, + /** PostgreSQL escape string escapes, for example {@code E'\n'} and {@code E'\x41'}. */ + POSTGRESQL_ESCAPE_STRING, + /** + * PostgreSQL Unicode escapes of a {@code U&'...'} string: the escape character followed by + * four hexadecimal digits or by {@code +} and six, and a doubled escape character. The + * escape character is a backslash unless {@link #getUnicodeEscapeCharacter()} names another + * one. + */ + POSTGRESQL_UNICODE_ESCAPE + } + private String value = ""; private String prefix = null; private String quoteStr = "'"; + private Escaping escaping = Escaping.STANDARD; + private Character unicodeEscapeCharacter = null; public StringValue() { // empty constructor @@ -36,10 +64,24 @@ public StringValue(String escapedValue) { && escapedValue.endsWith("'")) { value = escapedValue.substring(1, escapedValue.length() - 1); return; - } else if (escapedValue.length() >= 4 && escapedValue.startsWith("$$") - && escapedValue.endsWith("$$")) { - value = escapedValue.substring(2, escapedValue.length() - 2); - quoteStr = "$$"; + } else if (escapedValue.length() >= 2 && escapedValue.startsWith("\"") + && escapedValue.endsWith("\"")) { + // double quoted String Literals (Feature.allowDoubleQuotedStrings) + value = escapedValue.substring(1, escapedValue.length() - 1); + quoteStr = "\""; + return; + } + + String delimiter = getDollarQuoteDelimiter(escapedValue); + if (delimiter != null && escapedValue.length() >= 2 * delimiter.length() + && escapedValue.endsWith(delimiter)) { + quoteStr = delimiter; + value = escapedValue.substring(delimiter.length(), + escapedValue.length() - delimiter.length()); + return; + } + + if (readUnicodeEscapeString(escapedValue)) { return; } @@ -58,6 +100,72 @@ public StringValue(String escapedValue) { value = escapedValue; } + /** + * Reads {@code U&'body'} with an optional {@code UESCAPE 'c'} clause. Returns false, leaving + * this value unchanged, when the text has another shape. + */ + private boolean readUnicodeEscapeString(String text) { + if (!text.regionMatches(true, 0, UNICODE_ESCAPE_PREFIX + "'", 0, 3)) { + return false; + } + int close = 3; + while (close < text.length()) { + if (text.charAt(close) != '\'') { + close++; + } else if (close + 1 < text.length() && text.charAt(close + 1) == '\'') { + close += 2; + } else { + break; + } + } + if (close >= text.length()) { + return false; + } + String clause = text.substring(close + 1).trim(); + Character escape = null; + if (!clause.isEmpty()) { + if (!clause.regionMatches(true, 0, "UESCAPE", 0, 7)) { + return false; + } + String quoted = clause.substring(7).trim(); + if (quoted.length() != 3 || quoted.charAt(0) != '\'' || quoted.charAt(2) != '\'') { + return false; + } + escape = quoted.charAt(1); + } + prefix = UNICODE_ESCAPE_PREFIX; + value = text.substring(3, close); + escaping = Escaping.POSTGRESQL_UNICODE_ESCAPE; + unicodeEscapeCharacter = escape; + return true; + } + + /** + * Returns the opening PostgreSQL dollar-quote delimiter, or null if there is none. A tag + * follows unquoted identifier rules, excluding dollar signs. This method does not require the + * closing delimiter or inspect the body. + */ + public static String getDollarQuoteDelimiter(String text) { + if (text == null || text.length() < 2 || text.charAt(0) != '$') { + return null; + } + int end = text.indexOf('$', 1); + if (end < 0) { + return null; + } + for (int i = 1; i < end;) { + int character = text.codePointAt(i); + boolean valid = + i == 1 ? Character.isUnicodeIdentifierStart(character) || character == '_' + : Character.isUnicodeIdentifierPart(character); + if (!valid) { + return null; + } + i += Character.charCount(character); + } + return text.substring(0, end + 1); + } + public String getValue() { return value; } @@ -83,16 +191,377 @@ public StringValue setQuoteStr(String quoteStr) { return this; } + public Escaping getEscaping() { + return escaping; + } + + public void setEscaping(Escaping escaping) { + this.escaping = escaping == null ? Escaping.STANDARD : escaping; + } + + public StringValue withEscaping(Escaping escaping) { + setEscaping(escaping); + return this; + } + + /** + * Returns the escape character named by the {@code UESCAPE} clause of a PostgreSQL Unicode + * escape string, or null when the literal has no such clause and the escape character is a + * backslash. + */ + public Character getUnicodeEscapeCharacter() { + return unicodeEscapeCharacter; + } + + /** + * Sets the escape character rendered as {@code UESCAPE 'c'} after the literal; null removes the + * clause. It applies to {@link Escaping#POSTGRESQL_UNICODE_ESCAPE} values, whose + * {@link #getNotExcapedValue()} rejects a character that PostgreSQL rejects. + */ + public void setUnicodeEscapeCharacter(Character unicodeEscapeCharacter) { + this.unicodeEscapeCharacter = unicodeEscapeCharacter; + } + + public StringValue withUnicodeEscapeCharacter(Character unicodeEscapeCharacter) { + setUnicodeEscapeCharacter(unicodeEscapeCharacter); + return this; + } + + /** + * Returns the literal's characters with its escapes resolved: a doubled quote character and, + * depending on {@link #getEscaping()}, backslash or Unicode escape sequences. PostgreSQL octal + * and hexadecimal escapes denote bytes, which are decoded as UTF-8. Dollar-quoted bodies are + * returned unchanged. + * + * @throws IllegalArgumentException if a {@link Escaping#POSTGRESQL_UNICODE_ESCAPE} value has an + * escape that PostgreSQL rejects, or a {@link Escaping#POSTGRESQL_ESCAPE_STRING} value + * has a Unicode escape that PostgreSQL rejects; the parser rejects such literals + */ public String getNotExcapedValue() { - StringBuilder buffer = new StringBuilder(value); - int index = 0; - int deletesNum = 0; - while ((index = value.indexOf("''", index)) != -1) { - buffer.deleteCharAt(index - deletesNum); - index += 2; - deletesNum++; + if (quoteStr != null && quoteStr.startsWith("$")) { + return value; + } + char quote = "\"".equals(quoteStr) ? '"' : '\''; + if (escaping == Escaping.POSTGRESQL_UNICODE_ESCAPE) { + return unescapeUnicode(value, quote, + unicodeEscapeCharacter == null ? '\\' : unicodeEscapeCharacter); + } + Unescaped result = new Unescaped(value.length()); + int i = 0; + while (i < value.length()) { + char c = value.charAt(i); + if (c == '\\' && escaping != Escaping.STANDARD && i + 1 < value.length()) { + i = escaping == Escaping.MYSQL_BACKSLASH + ? appendMySqlEscape(result, value.charAt(i + 1), i + 1) + : appendPostgresEscape(result, value, i + 1); + } else if (c == quote && i + 1 < value.length() && value.charAt(i + 1) == quote) { + result.append(quote); + i += 2; + } else { + result.append(c); + i++; + } + } + return result.toString(); + } + + /** Appends the MySQL escape {@code c} at {@code index} and returns the index after it. */ + private static int appendMySqlEscape(Unescaped result, char c, int index) { + switch (c) { + case '0': + result.append('\0'); + break; + case 'b': + result.append('\b'); + break; + case 'n': + result.append('\n'); + break; + case 'r': + result.append('\r'); + break; + case 't': + result.append('\t'); + break; + case 'Z': + result.append((char) 0x1A); + break; + case '%': + case '_': + // kept with the backslash, so that LIKE patterns still match them literally + result.append('\\'); + result.append(c); + break; + default: + result.append(c); + } + return index + 1; + } + + /** Appends the PostgreSQL escape string escape at {@code index}; returns the next index. */ + private static int appendPostgresEscape(Unescaped result, String text, int index) { + char c = text.charAt(index); + switch (c) { + case 'b': + result.append('\b'); + return index + 1; + case 'f': + result.append('\f'); + return index + 1; + case 'n': + result.append('\n'); + return index + 1; + case 'r': + result.append('\r'); + return index + 1; + case 't': + result.append('\t'); + return index + 1; + case 'x': + return appendByte(result, text, index + 1, 16, 1, 2, c); + case 'u': + return appendCodePoint(result, text, index, 4); + case 'U': + return appendCodePoint(result, text, index, 8); + default: + if (c >= '0' && c <= '7') { + return appendByte(result, text, index, 8, 1, 3, c); + } + result.append(c); + return index + 1; + } + } + + /** + * Appends the byte spelled by {@code min} to {@code max} digits at {@code start}, or + * {@code fallback} when fewer digits follow, and returns the next index. + */ + private static int appendByte(Unescaped result, String text, int start, int radix, int min, + int max, char fallback) { + int end = start; + while (end < text.length() && end - start < max + && Character.digit(text.charAt(end), radix) >= 0) { + end++; + } + if (end - start < min) { + result.append(fallback); + return start; + } + result.appendByte(Integer.parseInt(text.substring(start, end), radix)); + return end; + } + + /** + * Appends the code point spelled by exactly {@code digits} hexadecimal digits after the escape + * letter at {@code index} and returns the next index. As in a Unicode escape string, the code + * point must be valid and a UTF-16 surrogate pair must be written as two consecutive escapes. + */ + private static int appendCodePoint(Unescaped result, String text, int index, int digits) { + int start = index + 1; + int end = start + digits; + long codePoint = parseHex(text, start, end); + if (codePoint < 0) { + throw new IllegalArgumentException("invalid Unicode escape at position " + (index - 1) + + ": Unicode escapes must be \\u followed by 4 or \\U by 8 hexadecimal digits"); + } + if (codePoint == 0 || codePoint > Character.MAX_CODE_POINT) { + throw new IllegalArgumentException( + "invalid Unicode escape value at position " + (index - 1)); + } + result.appendCodePoint((int) codePoint, index - 1); + return end; + } + + /** + * Resolves PostgreSQL Unicode escapes the way the server does: {@code escape} followed by four + * hexadecimal digits, or by {@code +} and six, names a code point, and a UTF-16 surrogate pair + * must be written as two consecutive escapes. A doubled escape or quote character stands for + * itself. + */ + private static String unescapeUnicode(String text, char quote, char escape) { + checkUnicodeEscapeCharacter(escape); + UnicodeUnescaped result = new UnicodeUnescaped(text.length()); + int i = 0; + while (i < text.length()) { + char c = text.charAt(i); + boolean doubled = i + 1 < text.length() && text.charAt(i + 1) == c; + if (c == escape && !doubled) { + i = appendUnicodeEscape(result, text, i, escape); + } else { + result.append(c, i); + i += doubled && (c == escape || c == quote) ? 2 : 1; + } + } + return result.finish(text.length()); + } + + private static void checkUnicodeEscapeCharacter(char escape) { + if (escape > 0x7F || isHexDigit(escape) || "+'\" \t\n\r\f\u000B".indexOf(escape) >= 0) { + throw new IllegalArgumentException("invalid Unicode escape character '" + escape + "'"); + } + } + + /** + * Appends the code point of the escape at {@code position}, {@code escape} followed by four + * hexadecimal digits or by {@code +} and six, and returns the next index. + */ + private static int appendUnicodeEscape(UnicodeUnescaped result, String text, int position, + char escape) { + boolean plus = position + 1 < text.length() && text.charAt(position + 1) == '+'; + int start = plus ? position + 2 : position + 1; + int end = start + (plus ? 6 : 4); + long codePoint = parseHex(text, start, end); + if (codePoint < 0) { + throw new IllegalArgumentException("invalid Unicode escape at position " + position + + ": Unicode escapes must be " + escape + "XXXX or " + escape + "+XXXXXX"); + } + if (codePoint == 0 || codePoint > Character.MAX_CODE_POINT) { + throw new IllegalArgumentException( + "invalid Unicode escape value at position " + position); + } + result.appendCodePoint((int) codePoint, position); + return end; + } + + private static IllegalArgumentException invalidSurrogatePair(int position) { + return new IllegalArgumentException( + "invalid Unicode surrogate pair at position " + position); + } + + private static boolean isHexDigit(char c) { + return c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'; + } + + /** Returns the ASCII hexadecimal number spelled by text[start, end), or -1. */ + private static long parseHex(String text, int start, int end) { + if (end > text.length()) { + return -1; + } + long number = 0; + for (int i = start; i < end; i++) { + char c = text.charAt(i); + if (!isHexDigit(c)) { + return -1; + } + number = number * 16 + Character.digit(c, 16); + } + return number; + } + + /** + * Collects unescaped characters; consecutive escaped bytes are decoded as UTF-8. A UTF-16 + * surrogate pair must be written as two consecutive Unicode escapes, as PostgreSQL requires. + */ + private static final class Unescaped { + private final StringBuilder chars; + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private char highSurrogate; + private int highSurrogatePosition; + + Unescaped(int capacity) { + chars = new StringBuilder(capacity); + } + + void append(char c) { + requireNoPendingSurrogate(); + flushBytes(); + chars.append(c); + } + + /** Appends the code point of the Unicode escape at {@code position}. */ + void appendCodePoint(int codePoint, int position) { + flushBytes(); + if (codePoint >= Character.MIN_LOW_SURROGATE + && codePoint <= Character.MAX_LOW_SURROGATE) { + if (highSurrogate == 0) { + throw invalidSurrogatePair(position); + } + chars.appendCodePoint(Character.toCodePoint(highSurrogate, (char) codePoint)); + highSurrogate = 0; + return; + } + requireNoPendingSurrogate(); + if (codePoint >= Character.MIN_HIGH_SURROGATE + && codePoint <= Character.MAX_HIGH_SURROGATE) { + highSurrogate = (char) codePoint; + highSurrogatePosition = position; + } else { + chars.appendCodePoint(codePoint); + } + } + + void appendByte(int value) { + requireNoPendingSurrogate(); + bytes.write(value); + } + + private void flushBytes() { + if (bytes.size() > 0) { + chars.append(new String(bytes.toByteArray(), StandardCharsets.UTF_8)); + bytes.reset(); + } + } + + private void requireNoPendingSurrogate() { + if (highSurrogate != 0) { + throw invalidSurrogatePair(highSurrogatePosition); + } + } + + @Override + public String toString() { + requireNoPendingSurrogate(); + flushBytes(); + return chars.toString(); + } + } + + /** + * Collects resolved Unicode escapes. A UTF-16 surrogate pair must be written as two consecutive + * escapes, as PostgreSQL requires. + */ + private static final class UnicodeUnescaped { + private final StringBuilder chars; + private char highSurrogate; + + UnicodeUnescaped(int capacity) { + chars = new StringBuilder(capacity); + } + + void append(char c, int position) { + requireNoPendingSurrogate(position); + chars.append(c); + } + + void appendCodePoint(int codePoint, int position) { + if (codePoint >= Character.MIN_LOW_SURROGATE + && codePoint <= Character.MAX_LOW_SURROGATE) { + if (highSurrogate == 0) { + throw invalidSurrogatePair(position); + } + chars.appendCodePoint(Character.toCodePoint(highSurrogate, (char) codePoint)); + highSurrogate = 0; + return; + } + requireNoPendingSurrogate(position); + if (codePoint >= Character.MIN_HIGH_SURROGATE + && codePoint <= Character.MAX_HIGH_SURROGATE) { + highSurrogate = (char) codePoint; + } else { + chars.appendCodePoint(codePoint); + } + } + + String finish(int position) { + requireNoPendingSurrogate(position); + return chars.toString(); + } + + private void requireNoPendingSurrogate(int position) { + if (highSurrogate != 0) { + throw invalidSurrogatePair(position); + } } - return buffer.toString(); } @Override @@ -102,7 +571,10 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { - return (prefix != null ? prefix : "") + quoteStr + value + quoteStr; + String unicodeEscape = unicodeEscapeCharacter != null + ? " UESCAPE '" + unicodeEscapeCharacter + "'" + : ""; + return (prefix != null ? prefix : "") + quoteStr + value + quoteStr + unicodeEscape; } public StringValue withPrefix(String prefix) { diff --git a/src/main/java/net/sf/jsqlparser/expression/StructType.java b/src/main/java/net/sf/jsqlparser/expression/StructType.java index 4bd38a6930..c2b59c51a3 100644 --- a/src/main/java/net/sf/jsqlparser/expression/StructType.java +++ b/src/main/java/net/sf/jsqlparser/expression/StructType.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; /* * STRUCT @@ -113,73 +114,62 @@ public StructType add(Expression expression, String aliasName) { } public StringBuilder appendTo(StringBuilder builder) { - if (dialect != Dialect.DUCKDB && keyword != null) { - builder.append(keyword); - } + return appendTo(builder, expression -> builder.append(expression)); + } - if (dialect != Dialect.DUCKDB && parameters != null && !parameters.isEmpty()) { - builder.append("<"); - int i = 0; - - for (Map.Entry e : parameters) { - if (0 < i++) { - builder.append(","); - } - // optional name - if (e.getKey() != null && !e.getKey().isEmpty()) { - builder.append(e.getKey()).append(" "); - } - - // mandatory type - builder.append(e.getValue()); + /** Renders current argument expressions through the caller's visitor. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + if (dialect == Dialect.DUCKDB) { + appendArguments(builder, expressions); + appendTypeParameters(builder, true); + } else { + if (keyword != null) { + builder.append(keyword); } - - builder.append(">"); + appendTypeParameters(builder, false); + appendArguments(builder, expressions); } + return builder; + } - if (arguments != null && !arguments.isEmpty()) { - - if (dialect == Dialect.DUCKDB) { - builder.append("{ "); - int i = 0; - for (SelectItem e : arguments) { - if (0 < i++) { - builder.append(","); - } - builder.append(e.getAlias().getName()); - builder.append(":"); - builder.append(e.getExpression()); - } - builder.append(" }"); - } else { - builder.append("("); - int i = 0; - for (SelectItem e : arguments) { - if (0 < i++) { - builder.append(","); - } - e.appendTo(builder); - } - - builder.append(")"); + private void appendTypeParameters(StringBuilder builder, boolean cast) { + if (parameters == null || parameters.isEmpty()) { + return; + } + builder.append(cast ? "::STRUCT( " : "<"); + int i = 0; + for (Map.Entry parameter : parameters) { + if (i++ > 0) { + builder.append(','); + } + if (cast || parameter.getKey() != null && !parameter.getKey().isEmpty()) { + builder.append(parameter.getKey()).append(' '); } + builder.append(parameter.getValue()); } + builder.append(cast ? ')' : '>'); + } - if (dialect == Dialect.DUCKDB && parameters != null && !parameters.isEmpty()) { - builder.append("::STRUCT( "); - int i = 0; - - for (Map.Entry e : parameters) { - if (0 < i++) { - builder.append(","); - } - builder.append(e.getKey()).append(" "); - builder.append(e.getValue()); + private void appendArguments(StringBuilder builder, Consumer expressions) { + if (arguments == null || arguments.isEmpty()) { + return; + } + boolean structLiteral = dialect == Dialect.DUCKDB; + builder.append(structLiteral ? "{ " : "("); + int i = 0; + for (SelectItem argument : arguments) { + if (i++ > 0) { + builder.append(','); + } + if (structLiteral) { + builder.append(argument.getAlias().getName()).append(':'); + } + expressions.accept(argument.getExpression()); + if (!structLiteral && argument.getAlias() != null) { + builder.append(argument.getAlias()); } - builder.append(")"); } - - return builder; + builder.append(structLiteral ? " }" : ")"); } @Override 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 0000000000..3d67d4ea38 --- /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 355e863053..a784518c50 100644 --- a/src/main/java/net/sf/jsqlparser/expression/TranscodingFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/TranscodingFunction.java @@ -14,10 +14,16 @@ import net.sf.jsqlparser.statement.create.table.ColDataType; import java.util.Objects; +import java.util.function.Consumer; public class TranscodingFunction extends ASTNodeAccessImpl implements Expression { private String keyword = "CONVERT"; - private boolean isTranscodeStyle = true; + + public enum Syntax { + USING, TYPE_FIRST, TYPE_LAST + } + + private Syntax syntax = Syntax.USING; private ColDataType colDataType; private Expression expression; private String transcodingName; @@ -39,7 +45,7 @@ public TranscodingFunction(String keyword, ColDataType colDataType, Expression e this.colDataType = colDataType; this.expression = expression; this.transcodingName = transcodingName; - this.isTranscodeStyle = false; + this.syntax = Syntax.TYPE_FIRST; } public TranscodingFunction(ColDataType colDataType, Expression expression, @@ -47,7 +53,7 @@ public TranscodingFunction(ColDataType colDataType, Expression expression, this.colDataType = colDataType; this.expression = expression; this.transcodingName = transcodingName; - this.isTranscodeStyle = false; + this.syntax = Syntax.TYPE_FIRST; } public TranscodingFunction() { @@ -100,11 +106,11 @@ public TranscodingFunction setColDataType(ColDataType colDataType) { } public boolean isTranscodeStyle() { - return isTranscodeStyle; + return syntax == Syntax.USING; } public TranscodingFunction setTranscodeStyle(boolean transcodeStyle) { - isTranscodeStyle = transcodeStyle; + syntax = transcodeStyle ? Syntax.USING : Syntax.TYPE_FIRST; return this; } @@ -112,27 +118,33 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { return expressionVisitor.visit(this, context); } + public Syntax getSyntax() { + return syntax; + } + + public TranscodingFunction setSyntax(Syntax syntax) { + this.syntax = Objects.requireNonNull(syntax); + return this; + } + public StringBuilder appendTo(StringBuilder builder) { - if (isTranscodeStyle) { - return builder - .append(keyword) - .append("( ") - .append(expression) - .append(" USING ") - .append(transcodingName) - .append(" )"); - } else { - return builder - .append(keyword) - .append("( ") - .append(colDataType) - .append(", ") - .append(expression) - .append(transcodingName != null && !transcodingName.isEmpty() - ? ", " + transcodingName - : "") - .append(" )"); + return appendTo(builder, builder::append); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) { + builder.append(keyword).append("( "); + if (syntax == Syntax.TYPE_FIRST) { + builder.append(colDataType).append(", "); + } + expressionRenderer.accept(expression); + if (syntax == Syntax.USING) { + builder.append(" USING ").append(transcodingName); + } else if (syntax == Syntax.TYPE_LAST) { + builder.append(", ").append(colDataType); + } else if (transcodingName != null && !transcodingName.isEmpty()) { + builder.append(", ").append(transcodingName); } + return builder.append(" )"); } @Override diff --git a/src/main/java/net/sf/jsqlparser/expression/WindowDefinition.java b/src/main/java/net/sf/jsqlparser/expression/WindowDefinition.java index 60760baee0..cbf6253548 100644 --- a/src/main/java/net/sf/jsqlparser/expression/WindowDefinition.java +++ b/src/main/java/net/sf/jsqlparser/expression/WindowDefinition.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; @@ -22,6 +23,7 @@ public class WindowDefinition implements Serializable { final PartitionByClause partitionBy = new PartitionByClause(); WindowElement windowElement = null; private String windowName; + private String baseWindowName; public OrderByClause getOrderBy() { return orderBy; @@ -60,6 +62,31 @@ public void setPartitionExpressionList(ExpressionList partitionExpre partitionBy.setExpressions(partitionExpressionList, brackets); } + /** DuckDB: the window this one extends, as w in OVER (w ORDER BY b), or {@code null}. */ + public String getBaseWindowName() { + return baseWindowName; + } + + public WindowDefinition setBaseWindowName(String baseWindowName) { + this.baseWindowName = baseWindowName; + return this; + } + + /** Appends the base window name, separated from any following part. */ + public void appendBaseWindowNameTo(StringBuilder builder) { + if (baseWindowName != null) { + builder.append(baseWindowName); + boolean more = partitionBy.getPartitionExpressionList() != null + && !partitionBy.getPartitionExpressionList().isEmpty() + || orderBy.getOrderByElements() != null + && !orderBy.getOrderByElements().isEmpty() + || windowElement != null; + if (more) { + builder.append(' '); + } + } + } + public String getWindowName() { return windowName; } @@ -73,6 +100,30 @@ public WindowDefinition withWindowName(String windowName) { return this; } + /** Returns the partition, order and frame expressions for both inline and named windows. */ + public List getAllExpressions() { + List expressions = new ArrayList<>(partitionBy); + if (getOrderByElements() != null) { + for (OrderByElement element : getOrderByElements()) { + expressions.add(element.getExpression()); + } + } + if (windowElement != null) { + if (windowElement.getRange() != null) { + addOffsetExpression(expressions, windowElement.getRange().getStart()); + addOffsetExpression(expressions, windowElement.getRange().getEnd()); + } + addOffsetExpression(expressions, windowElement.getOffset()); + } + return expressions; + } + + private static void addOffsetExpression(List expressions, WindowOffset offset) { + if (offset != null && offset.getExpression() != null) { + expressions.add(offset.getExpression()); + } + } + @Override public String toString() { StringBuilder b = new StringBuilder(); @@ -80,6 +131,7 @@ public String toString() { b.append(windowName).append(" AS "); } b.append("("); + appendBaseWindowNameTo(b); partitionBy.toStringPartitionBy(b); orderBy.toStringOrderByElements(b); diff --git a/src/main/java/net/sf/jsqlparser/expression/XMLSerializeExpr.java b/src/main/java/net/sf/jsqlparser/expression/XMLSerializeExpr.java index d2534e62d2..62a4e703aa 100644 --- a/src/main/java/net/sf/jsqlparser/expression/XMLSerializeExpr.java +++ b/src/main/java/net/sf/jsqlparser/expression/XMLSerializeExpr.java @@ -9,7 +9,9 @@ */ package net.sf.jsqlparser.expression; +import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import static java.util.stream.Collectors.joining; @@ -19,9 +21,19 @@ public class XMLSerializeExpr extends ASTNodeAccessImpl implements Expression { + public enum SerializationMode { + CONTENT, DOCUMENT + } + private Expression expression; private List orderByElements; private ColDataType dataType; + private SerializationMode serializationMode; + private StringValue encoding; + private StringValue version; + private Boolean indent; + private LongValue indentSize; + private Boolean showDefaults; @Override public T accept(ExpressionVisitor expressionVisitor, S context) { @@ -52,11 +64,141 @@ public void setDataType(ColDataType dataType) { this.dataType = dataType; } + /** Null retains the legacy XMLAGG(XMLTEXT(...)) form and its existing expression getter. */ + public SerializationMode getSerializationMode() { + return serializationMode; + } + + public void setSerializationMode(SerializationMode serializationMode) { + this.serializationMode = serializationMode; + } + + public StringValue getEncoding() { + return encoding; + } + + public void setEncoding(StringValue encoding) { + this.encoding = encoding; + } + + public StringValue getVersion() { + return version; + } + + public void setVersion(StringValue version) { + this.version = version; + } + + /** Null preserves omission, true is INDENT, and false is NO INDENT. */ + public Boolean getIndent() { + return indent; + } + + public void setIndent(Boolean indent) { + this.indent = indent; + } + + public LongValue getIndentSize() { + return indentSize; + } + + public void setIndentSize(LongValue indentSize) { + this.indentSize = indentSize; + } + + /** Null preserves omission, true is SHOW DEFAULTS, and false is HIDE DEFAULTS. */ + public Boolean getShowDefaults() { + return showDefaults; + } + + public void setShowDefaults(Boolean showDefaults) { + this.showDefaults = showDefaults; + } + + /** Shared child discovery for expression, table-name and validation visitors. */ + public List getExpressions() { + List result = new ArrayList<>(); + if (expression != null) { + result.add(expression); + } + if (orderByElements != null) { + for (OrderByElement orderBy : orderByElements) { + result.add(orderBy.getExpression()); + } + } + if (encoding != null) { + result.add(encoding); + } + if (version != null) { + result.add(version); + } + if (indentSize != null) { + result.add(indentSize); + } + return result; + } + + /** Render both forms without bypassing custom expression or ORDER BY deparsers. */ + public StringBuilder appendTo(StringBuilder sql, Consumer expressionWriter, + Consumer> orderByWriter) { + validateOptions(); + sql.append("xmlserialize("); + if (serializationMode == null) { + sql.append("xmlagg(xmltext("); + expressionWriter.accept(expression); + sql.append(")"); + if (orderByElements != null) { + orderByWriter.accept(orderByElements); + } + sql.append(") AS ").append(dataType); + } else { + sql.append(serializationMode).append(" "); + expressionWriter.accept(expression); + if (dataType != null) { + sql.append(" AS ").append(dataType); + } + if (encoding != null) { + sql.append(" ENCODING "); + expressionWriter.accept(encoding); + } + if (version != null) { + sql.append(" VERSION "); + expressionWriter.accept(version); + } + if (indent != null) { + sql.append(indent ? " INDENT" : " NO INDENT"); + if (indent && indentSize != null) { + sql.append(" SIZE = "); + expressionWriter.accept(indentSize); + } + } + if (showDefaults != null) { + sql.append(showDefaults ? " SHOW DEFAULTS" : " HIDE DEFAULTS"); + } + } + return sql.append(")"); + } + + public void validateOptions() { + if (indentSize != null && (!Boolean.TRUE.equals(indent) || indentSize.getValue() < 0)) { + throw new IllegalArgumentException( + "An indentation size requires INDENT and must be nonnegative"); + } + if (serializationMode == null && (encoding != null || version != null || indent != null + || indentSize != null || showDefaults != null)) { + throw new IllegalArgumentException("Serialization options require CONTENT or DOCUMENT"); + } + if (serializationMode != null && orderByElements != null && !orderByElements.isEmpty()) { + throw new IllegalArgumentException( + "ORDER BY belongs inside the serialized XMLAGG expression"); + } + } + @Override public String toString() { - return "xmlserialize(xmlagg(xmltext(" + expression + ")" - + (orderByElements != null ? " ORDER BY " + orderByElements.stream() - .map(OrderByElement::toString).collect(joining(", ")) : "") - + ") AS " + dataType + ")"; + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append, orderBy -> sql.append(" ORDER BY ") + .append(orderBy.stream().map(OrderByElement::toString).collect(joining(", ")))) + .toString(); } } diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/arithmetic/Exponentiation.java b/src/main/java/net/sf/jsqlparser/expression/operators/arithmetic/Exponentiation.java new file mode 100644 index 0000000000..321e014879 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/operators/arithmetic/Exponentiation.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.operators.arithmetic; + +import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; + +/** DuckDB's {@code a ** b}, {@code a} raised to the power {@code b}. */ +public class Exponentiation extends BinaryExpression { + + public Exponentiation() {} + + public Exponentiation(Expression leftExpression, Expression rightExpression) { + super(leftExpression, rightExpression); + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String getStringExpression() { + return "**"; + } + + @Override + public Exponentiation withLeftExpression(Expression arg0) { + return (Exponentiation) super.withLeftExpression(arg0); + } + + @Override + public Exponentiation withRightExpression(Expression arg0) { + return (Exponentiation) super.withRightExpression(arg0); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/Between.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/Between.java index 8998863ef2..ff24e0eba3 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/Between.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/Between.java @@ -16,7 +16,7 @@ /** * A "BETWEEN" expr1 expr2 statement */ -public class Between extends ASTNodeAccessImpl implements Expression { +public class Between extends ASTNodeAccessImpl implements Expression, SupportsOldOracleJoinSyntax { private Expression leftExpression; private boolean not = false; @@ -82,7 +82,8 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { - return leftExpression + " " + (not ? "NOT " : "") + "BETWEEN " + return (oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : "") + leftExpression + " " + + (not ? "NOT " : "") + "BETWEEN " + (usingSymmetric ? "SYMMETRIC " : "") + (usingAsymmetric ? "ASYMMETRIC " : "") + betweenExpressionStart + " AND " @@ -120,4 +121,39 @@ public E getBetweenExpressionStart(Class type) { public E getLeftExpression(Class type) { return type.cast(getLeftExpression()); } + + private int oraclePriorPosition = NO_ORACLE_PRIOR; + + @Override + public int getOldOracleJoinSyntax() { + return NO_ORACLE_JOIN; + } + + @Override + public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) { + throw new IllegalArgumentException( + "oracle join operator (+) is not supported on this condition"); + } + + @Override + public Between withOldOracleJoinSyntax(int oldOracleJoinSyntax) { + setOldOracleJoinSyntax(oldOracleJoinSyntax); + return this; + } + + @Override + public int getOraclePriorPosition() { + return oraclePriorPosition; + } + + @Override + public void setOraclePriorPosition(int oraclePriorPosition) { + this.oraclePriorPosition = oraclePriorPosition; + } + + public Between withOraclePriorPosition(int oraclePriorPosition) { + setOraclePriorPosition(oraclePriorPosition); + return this; + } + } diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/EqualsTo.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/EqualsTo.java index 8f80f1be88..19f238ffbb 100755 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/EqualsTo.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/EqualsTo.java @@ -18,6 +18,11 @@ public EqualsTo() { super("="); } + /** {@code operator} is {@code =} or DuckDB's {@code ==}. */ + public EqualsTo(String operator) { + super(operator); + } + public EqualsTo(Expression left, Expression right) { this(); setLeftExpression(left); 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 0bf79f0ec8..8c1a8d97ef 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 @@ -75,13 +75,13 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { // Build a list of matched columns - String columnsListCommaSeperated = ""; + StringBuilder columnsListCommaSeperated = new StringBuilder(); Iterator iterator = this._matchColumns.iterator(); while (iterator.hasNext()) { Column col = iterator.next(); - columnsListCommaSeperated += col.getFullyQualifiedName(); + columnsListCommaSeperated.append(col.getFullyQualifiedName()); if (iterator.hasNext()) { - columnsListCommaSeperated += ","; + columnsListCommaSeperated.append(","); } } diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/InExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/InExpression.java index 743d8a0aa7..c920c2b872 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/InExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/InExpression.java @@ -22,6 +22,8 @@ public class InExpression extends ASTNodeAccessImpl private Expression rightExpression; private int oldOracleJoinSyntax = NO_ORACLE_JOIN; + private int oraclePriorPosition = NO_ORACLE_PRIOR; + public InExpression() {} public InExpression(Expression leftExpression, Expression rightExpression) { @@ -94,6 +96,9 @@ private String getLeftExpressionString() { @Override public String toString() { StringBuilder statementBuilder = new StringBuilder(); + if (oraclePriorPosition == ORACLE_PRIOR_START) { + statementBuilder.append("PRIOR "); + } statementBuilder.append(getLeftExpressionString()); statementBuilder.append(" "); @@ -110,14 +115,12 @@ public String toString() { @Override public int getOraclePriorPosition() { - return SupportsOldOracleJoinSyntax.NO_ORACLE_PRIOR; + return oraclePriorPosition; } @Override public void setOraclePriorPosition(int priorPosition) { - if (priorPosition != SupportsOldOracleJoinSyntax.NO_ORACLE_PRIOR) { - throw new IllegalArgumentException("unexpected prior for oracle found"); - } + this.oraclePriorPosition = priorPosition; } public InExpression withRightExpression(Expression rightExpression) { diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.java new file mode 100644 index 0000000000..0f05edcb60 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.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.operators.relational; + +import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; + +/** + * The PostgreSQL # binary operator: the geometric intersection of lseg / line / box + * (documentation Table 9.36) and the integer bitwise exclusive OR (Table 9.4). + */ +public class Intersects extends BinaryExpression { + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String getStringExpression() { + return "#"; + } + + @Override + public Intersects withLeftExpression(Expression expression) { + return (Intersects) super.withLeftExpression(expression); + } + + @Override + public Intersects withRightExpression(Expression expression) { + return (Intersects) super.withRightExpression(expression); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsBooleanExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsBooleanExpression.java index 3c83365336..6cf101bd9a 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsBooleanExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsBooleanExpression.java @@ -13,7 +13,8 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; -public class IsBooleanExpression extends ASTNodeAccessImpl implements Expression { +public class IsBooleanExpression extends ASTNodeAccessImpl + implements Expression, SupportsOldOracleJoinSyntax { private Expression leftExpression; private boolean not = false; @@ -50,10 +51,11 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { + String prior = oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : ""; if (isTrue()) { - return leftExpression + " IS" + (not ? " NOT" : "") + " TRUE"; + return prior + leftExpression + " IS" + (not ? " NOT" : "") + " TRUE"; } else { - return leftExpression + " IS" + (not ? " NOT" : "") + " FALSE"; + return prior + leftExpression + " IS" + (not ? " NOT" : "") + " FALSE"; } } @@ -75,4 +77,39 @@ public IsBooleanExpression withNot(boolean not) { public E getLeftExpression(Class type) { return type.cast(getLeftExpression()); } + + private int oraclePriorPosition = NO_ORACLE_PRIOR; + + @Override + public int getOldOracleJoinSyntax() { + return NO_ORACLE_JOIN; + } + + @Override + public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) { + throw new IllegalArgumentException( + "oracle join operator (+) is not supported on this condition"); + } + + @Override + public IsBooleanExpression withOldOracleJoinSyntax(int oldOracleJoinSyntax) { + setOldOracleJoinSyntax(oldOracleJoinSyntax); + return this; + } + + @Override + public int getOraclePriorPosition() { + return oraclePriorPosition; + } + + @Override + public void setOraclePriorPosition(int oraclePriorPosition) { + this.oraclePriorPosition = oraclePriorPosition; + } + + public IsBooleanExpression withOraclePriorPosition(int oraclePriorPosition) { + setOraclePriorPosition(oraclePriorPosition); + return this; + } + } diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsDistinctExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsDistinctExpression.java index 60add6b6ed..12bc3027cd 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsDistinctExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsDistinctExpression.java @@ -12,7 +12,7 @@ import net.sf.jsqlparser.expression.BinaryExpression; import net.sf.jsqlparser.expression.ExpressionVisitor; -public class IsDistinctExpression extends BinaryExpression { +public class IsDistinctExpression extends BinaryExpression implements SupportsOldOracleJoinSyntax { private boolean not = false; @@ -36,7 +36,43 @@ public String getStringExpression() { @Override public String toString() { - String retval = getLeftExpression() + getStringExpression() + getRightExpression(); + String retval = (oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : "") + + getLeftExpression() + getStringExpression() + getRightExpression(); return retval; } + + private int oraclePriorPosition = NO_ORACLE_PRIOR; + + @Override + public int getOldOracleJoinSyntax() { + return NO_ORACLE_JOIN; + } + + @Override + public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) { + throw new IllegalArgumentException( + "oracle join operator (+) is not supported on this condition"); + } + + @Override + public IsDistinctExpression withOldOracleJoinSyntax(int oldOracleJoinSyntax) { + setOldOracleJoinSyntax(oldOracleJoinSyntax); + return this; + } + + @Override + public int getOraclePriorPosition() { + return oraclePriorPosition; + } + + @Override + public void setOraclePriorPosition(int oraclePriorPosition) { + this.oraclePriorPosition = oraclePriorPosition; + } + + public IsDistinctExpression withOraclePriorPosition(int oraclePriorPosition) { + setOraclePriorPosition(oraclePriorPosition); + return this; + } + } diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsJsonExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsJsonExpression.java new file mode 100644 index 0000000000..e70b0bdd2d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsJsonExpression.java @@ -0,0 +1,98 @@ +/*- + * #%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.operators.relational; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** SQL/JSON type and key uniqueness predicate; the JSON value is not evaluated by the parser. */ +public class IsJsonExpression extends ASTNodeAccessImpl implements Expression { + public enum Type { + VALUE, SCALAR, ARRAY, OBJECT + } + + public enum UniqueKeys { + WITH, WITHOUT + } + + private Expression leftExpression; + private boolean not; + private Type type; + private UniqueKeys uniqueKeys; + private boolean useKeysKeyword = true; + + public Expression getLeftExpression() { + return leftExpression; + } + + public void setLeftExpression(Expression leftExpression) { + this.leftExpression = leftExpression; + } + + public boolean isNot() { + return not; + } + + public void setNot(boolean not) { + this.not = not; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public UniqueKeys getUniqueKeys() { + return uniqueKeys; + } + + public void setUniqueKeys(UniqueKeys uniqueKeys) { + this.uniqueKeys = uniqueKeys; + } + + public boolean isUseKeysKeyword() { + return useKeysKeyword; + } + + public void setUseKeysKeyword(boolean useKeysKeyword) { + this.useKeysKeyword = useKeysKeyword; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) { + expressionRenderer.accept(leftExpression); + builder.append(not ? " IS NOT JSON" : " IS JSON"); + if (type != null) { + builder.append(' ').append(type); + } + if (uniqueKeys != null) { + builder.append(' ').append(uniqueKeys).append(" UNIQUE"); + if (useKeysKeyword) { + builder.append(" KEYS"); + } + } + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + @Override + public T accept(ExpressionVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsNullExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsNullExpression.java index 393f9f1944..f044c436ae 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsNullExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsNullExpression.java @@ -14,7 +14,8 @@ import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Column; -public class IsNullExpression extends ASTNodeAccessImpl implements Expression { +public class IsNullExpression extends ASTNodeAccessImpl + implements Expression, SupportsOldOracleJoinSyntax { private Expression leftExpression; private boolean not = false; @@ -72,12 +73,13 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { + String prior = oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : ""; if (useNotNull) { - return leftExpression + " NOTNULL"; + return prior + leftExpression + " NOTNULL"; } else if (useIsNull) { - return leftExpression + (not ? " NOT" : "") + " ISNULL"; + return prior + leftExpression + (not ? " NOT" : "") + " ISNULL"; } else { - return leftExpression + " IS " + (not ? "NOT " : "") + "NULL"; + return prior + leftExpression + " IS " + (not ? "NOT " : "") + "NULL"; } } @@ -99,4 +101,39 @@ public IsNullExpression withNot(boolean not) { public E getLeftExpression(Class type) { return type.cast(getLeftExpression()); } + + private int oraclePriorPosition = NO_ORACLE_PRIOR; + + @Override + public int getOldOracleJoinSyntax() { + return NO_ORACLE_JOIN; + } + + @Override + public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) { + throw new IllegalArgumentException( + "oracle join operator (+) is not supported on this condition"); + } + + @Override + public IsNullExpression withOldOracleJoinSyntax(int oldOracleJoinSyntax) { + setOldOracleJoinSyntax(oldOracleJoinSyntax); + return this; + } + + @Override + public int getOraclePriorPosition() { + return oraclePriorPosition; + } + + @Override + public void setOraclePriorPosition(int oraclePriorPosition) { + this.oraclePriorPosition = oraclePriorPosition; + } + + public IsNullExpression withOraclePriorPosition(int oraclePriorPosition) { + setOraclePriorPosition(oraclePriorPosition); + return this; + } + } diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpression.java index 506d6e2466..06abbbcb55 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpression.java @@ -13,7 +13,8 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; -public class IsUnknownExpression extends ASTNodeAccessImpl implements Expression { +public class IsUnknownExpression extends ASTNodeAccessImpl + implements Expression, SupportsOldOracleJoinSyntax { private Expression leftExpression; private boolean isNot = false; @@ -41,7 +42,8 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { - return leftExpression + " IS" + (isNot ? " NOT" : "") + " UNKNOWN"; + return (oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : "") + leftExpression + " IS" + + (isNot ? " NOT" : "") + " UNKNOWN"; } public IsUnknownExpression withLeftExpression(Expression leftExpression) { @@ -57,4 +59,39 @@ public IsUnknownExpression withNot(boolean isNot) { public E getLeftExpression(Class type) { return type.cast(getLeftExpression()); } + + private int oraclePriorPosition = NO_ORACLE_PRIOR; + + @Override + public int getOldOracleJoinSyntax() { + return NO_ORACLE_JOIN; + } + + @Override + public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) { + throw new IllegalArgumentException( + "oracle join operator (+) is not supported on this condition"); + } + + @Override + public IsUnknownExpression withOldOracleJoinSyntax(int oldOracleJoinSyntax) { + setOldOracleJoinSyntax(oldOracleJoinSyntax); + return this; + } + + @Override + public int getOraclePriorPosition() { + return oraclePriorPosition; + } + + @Override + public void setOraclePriorPosition(int oraclePriorPosition) { + this.oraclePriorPosition = oraclePriorPosition; + } + + public IsUnknownExpression withOraclePriorPosition(int oraclePriorPosition) { + setOraclePriorPosition(oraclePriorPosition); + 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 770db3de39..125e38a825 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 @@ -10,14 +10,16 @@ package net.sf.jsqlparser.expression.operators.relational; import java.util.Locale; +import java.util.regex.Pattern; import net.sf.jsqlparser.expression.BinaryExpression; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; -public class LikeExpression extends BinaryExpression { +public class LikeExpression extends BinaryExpression implements SupportsOldOracleJoinSyntax { private boolean not = false; private boolean useBinary = false; private Expression escapeExpression = null; + private boolean useOperator = false; private KeyWord likeKeyWord = KeyWord.LIKE; public boolean isNot() { @@ -48,11 +50,64 @@ public String getStringExpression() { return likeKeyWord.toString(); } + /** + * The operator spelling of this condition when it was written as one and has one, e.g. + * {@code !~~} for {@code NOT LIKE}; otherwise {@code null} and the keyword form is used. + */ + public String getLikeOperator() { + if (!useOperator) { + return null; + } + switch (likeKeyWord) { + case LIKE: + return not ? "!~~" : "~~"; + case ILIKE: + return not ? "!~~*" : "~~*"; + case GLOB: + return not ? null : "~~~"; + default: + return null; + } + } + + /** + * Sets the keyword and {@code NOT} from DuckDB's and PostgreSQL's operator spelling: {@code ~~} + * is {@code LIKE}, {@code ~~*} {@code ILIKE}, {@code ~~~} {@code GLOB}, and a leading {@code !} + * negates the first two. The operator is printed while it matches the keyword and {@code NOT}. + */ + public LikeExpression setLikeOperator(String likeOperator) { + if (likeOperator == null) { + throw new IllegalArgumentException("The LIKE operator must not be null"); + } + switch (likeOperator) { + case "~~": + case "!~~": + likeKeyWord = KeyWord.LIKE; + break; + case "~~*": + case "!~~*": + likeKeyWord = KeyWord.ILIKE; + break; + case "~~~": + likeKeyWord = KeyWord.GLOB; + break; + default: + throw new IllegalArgumentException("Unknown LIKE operator: " + likeOperator); + } + this.not = likeOperator.startsWith("!"); + this.useOperator = true; + return this; + } + @Override public String toString() { - String retval = getLeftExpression() + " " + (not ? "NOT " : "") - + (likeKeyWord == KeyWord.SIMILAR_TO ? "SIMILAR TO" : likeKeyWord) + " " - + (useBinary ? "BINARY " : "") + getRightExpression(); + String likeOperator = getLikeOperator(); + String retval = (oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : "") + + getLeftExpression() + " " + + (likeOperator != null ? likeOperator + : (not ? "NOT " : "") + + (likeKeyWord == KeyWord.SIMILAR_TO ? "SIMILAR TO" : likeKeyWord)) + + " " + (useBinary ? "BINARY " : "") + getRightExpression(); if (escapeExpression != null) { retval += " ESCAPE " + escapeExpression; } @@ -118,11 +173,48 @@ public LikeExpression withRightExpression(Expression arg0) { } public enum KeyWord { - LIKE, ILIKE, RLIKE, REGEXP_LIKE, REGEXP, SIMILAR_TO, MATCH_ANY, MATCH_ALL, MATCH_PHRASE, MATCH_PHRASE_PREFIX, MATCH_REGEXP; + LIKE, ILIKE, RLIKE, REGEXP_LIKE, REGEXP, GLOB, SIMILAR_TO, MATCH_ANY, MATCH_ALL, MATCH_PHRASE, MATCH_PHRASE_PREFIX, MATCH_REGEXP; + + private static final Pattern WHITESPACE = Pattern.compile("\\s+"); public static KeyWord from(String keyword) { return Enum.valueOf(KeyWord.class, - keyword.toUpperCase(Locale.ROOT).replaceAll("\\s+", "_")); + WHITESPACE.matcher(keyword.toUpperCase(Locale.ROOT)).replaceAll("_")); } } + + private int oraclePriorPosition = NO_ORACLE_PRIOR; + + @Override + public int getOldOracleJoinSyntax() { + return NO_ORACLE_JOIN; + } + + @Override + public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) { + throw new IllegalArgumentException( + "oracle join operator (+) is not supported on this condition"); + } + + @Override + public LikeExpression withOldOracleJoinSyntax(int oldOracleJoinSyntax) { + setOldOracleJoinSyntax(oldOracleJoinSyntax); + return this; + } + + @Override + public int getOraclePriorPosition() { + return oraclePriorPosition; + } + + @Override + public void setOraclePriorPosition(int oraclePriorPosition) { + this.oraclePriorPosition = oraclePriorPosition; + } + + public LikeExpression withOraclePriorPosition(int oraclePriorPosition) { + setOraclePriorPosition(oraclePriorPosition); + return this; + } + } diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/MemberOfExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/MemberOfExpression.java index d602e62788..87a54dd072 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/MemberOfExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/MemberOfExpression.java @@ -13,7 +13,8 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; -public class MemberOfExpression extends ASTNodeAccessImpl implements Expression { +public class MemberOfExpression extends ASTNodeAccessImpl + implements Expression, SupportsOldOracleJoinSyntax { Expression leftExpression; Expression rightExpression; @@ -53,11 +54,47 @@ public MemberOfExpression setNot(boolean not) { @Override public String toString() { - return leftExpression + " MEMBER OF " + rightExpression; + return (oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : "") + leftExpression + + " MEMBER OF " + rightExpression; } @Override public T accept(ExpressionVisitor expressionVisitor, S context) { return expressionVisitor.visit(this, context); } + + private int oraclePriorPosition = NO_ORACLE_PRIOR; + + @Override + public int getOldOracleJoinSyntax() { + return NO_ORACLE_JOIN; + } + + @Override + public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) { + throw new IllegalArgumentException( + "oracle join operator (+) is not supported on this condition"); + } + + @Override + public MemberOfExpression withOldOracleJoinSyntax(int oldOracleJoinSyntax) { + setOldOracleJoinSyntax(oldOracleJoinSyntax); + return this; + } + + @Override + public int getOraclePriorPosition() { + return oraclePriorPosition; + } + + @Override + public void setOraclePriorPosition(int oraclePriorPosition) { + this.oraclePriorPosition = oraclePriorPosition; + } + + public MemberOfExpression withOraclePriorPosition(int oraclePriorPosition) { + setOraclePriorPosition(oraclePriorPosition); + return this; + } + } diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/SimilarToExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/SimilarToExpression.java index 0818d75c0a..fbb50f25d5 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/SimilarToExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/SimilarToExpression.java @@ -13,7 +13,7 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; -public class SimilarToExpression extends BinaryExpression { +public class SimilarToExpression extends BinaryExpression implements SupportsOldOracleJoinSyntax { private boolean not = false; private String escape = null; @@ -38,7 +38,8 @@ public String getStringExpression() { @Override public String toString() { - String retval = getLeftExpression() + " " + (not ? "NOT " : "") + getStringExpression() + String retval = (oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : "") + + getLeftExpression() + " " + (not ? "NOT " : "") + getStringExpression() + " " + getRightExpression(); if (escape != null) { retval += " ESCAPE " + "'" + escape + "'"; @@ -74,4 +75,39 @@ public SimilarToExpression withLeftExpression(Expression arg0) { public SimilarToExpression withRightExpression(Expression arg0) { return (SimilarToExpression) super.withRightExpression(arg0); } + + private int oraclePriorPosition = NO_ORACLE_PRIOR; + + @Override + public int getOldOracleJoinSyntax() { + return NO_ORACLE_JOIN; + } + + @Override + public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) { + throw new IllegalArgumentException( + "oracle join operator (+) is not supported on this condition"); + } + + @Override + public SimilarToExpression withOldOracleJoinSyntax(int oldOracleJoinSyntax) { + setOldOracleJoinSyntax(oldOracleJoinSyntax); + return this; + } + + @Override + public int getOraclePriorPosition() { + return oraclePriorPosition; + } + + @Override + public void setOraclePriorPosition(int oraclePriorPosition) { + this.oraclePriorPosition = oraclePriorPosition; + } + + public SimilarToExpression withOraclePriorPosition(int oraclePriorPosition) { + setOraclePriorPosition(oraclePriorPosition); + return this; + } + } diff --git a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java index 6ef61996b6..3a1ad53294 100644 --- a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java +++ b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.parser; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -26,6 +29,26 @@ public void setASTNode(Node node) { this.node = node; } + /** + * The comments preceding this node's first token, in source order, with the original token + * images. Line comments do not include their line terminator; callers must add a line break + * when prepending them to SQL. Empty for nodes built programmatically, which carry no AST. + */ + public List getLeadingComments() { + Node node = getASTNode(); + if (node == null) { + return new ArrayList<>(); + } + List comments = new ArrayList<>(); + for (Token specialToken = + node.jjtGetFirstToken().specialToken; specialToken != null; specialToken = + specialToken.specialToken) { + comments.add(specialToken.image); + } + Collections.reverse(comments); + return comments; + } + public StringBuilder appendTo(StringBuilder builder) { // don't add spaces around the following punctuation final Set punctuation = new TreeSet<>(Set.of(".", "[", "]")); @@ -48,22 +71,20 @@ public StringBuilder appendTo(StringBuilder builder) { return builder; } + /** + * The nearest enclosing object in the tree, or null when there is none. A node that carries + * only a token range, for an object no grammar rule opens a tree node for, has no parent. + */ public ASTNodeAccess getParent() { - Node parent = (Node) node.jjtGetParent(); - while (parent.jjtGetValue() == null) { - parent = (Node) parent.jjtGetParent(); - } - - return ASTNodeAccess.class.cast(parent.jjtGetValue()); + return getParent(ASTNodeAccess.class); } public T getParent(Class clazz) { - Node parent = (Node) node.jjtGetParent(); - while (parent.jjtGetValue() == null || !clazz.isInstance(parent.jjtGetValue())) { + Node parent = node == null ? null : (Node) node.jjtGetParent(); + while (parent != null && !clazz.isInstance(parent.jjtGetValue())) { parent = (Node) parent.jjtGetParent(); } - - return clazz.cast(parent.jjtGetValue()); + return parent == null ? null : clazz.cast(parent.jjtGetValue()); } @Override diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 45580cb5eb..7cee29a5e8 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -11,9 +11,14 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.statement.upsert.UpsertType; import java.util.ArrayList; +import java.util.Collections; +import java.util.Arrays; +import java.util.EnumSet; import java.util.List; +import java.util.Set; public abstract class AbstractJSqlParser

{ @@ -21,8 +26,52 @@ public abstract class AbstractJSqlParser

{ protected boolean errorRecovery = false; protected List parseErrors = new ArrayList<>(); + public enum AdjacentStringLiterals { + OFF, NEWLINE, WHITESPACE + } + public enum Dialect { - ORACLE, EXASOL + ANSI_SQL(AdjacentStringLiterals.NEWLINE), ORACLE, MYSQL(AdjacentStringLiterals.OFF, + Feature.allowBackslashEscapeCharacter, + Feature.allowHashLineComments, + Feature.allowDoubleQuotedStrings), MARIADB(AdjacentStringLiterals.OFF, + Feature.allowBackslashEscapeCharacter, + Feature.allowHashLineComments, + Feature.allowDoubleQuotedStrings), SQLSERVER(AdjacentStringLiterals.OFF, + Feature.allowSquareBracketQuotation), POSTGRESQL( + AdjacentStringLiterals.NEWLINE, + Feature.allowDollarQuotedStringTags, + Feature.allowDigitSeparators), H2, EXASOL, BIGQUERY( + AdjacentStringLiterals.WHITESPACE, + Feature.allowDoubleQuotedStrings, + Feature.allowHashLineComments, + Feature.allowBackslashEscapeCharacter), DATABRICKS( + AdjacentStringLiterals.WHITESPACE, + Feature.allowDoubleQuotedStrings, + Feature.allowBackslashEscapeCharacter), SNOWFLAKE( + Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB, TERADATA, SQLITE; + + private final Set lexerFeatures; + private final AdjacentStringLiterals adjacentStringLiterals; + + Dialect(AdjacentStringLiterals adjacentStringLiterals, Feature... lexerFeatures) { + this.adjacentStringLiterals = adjacentStringLiterals; + this.lexerFeatures = lexerFeatures.length == 0 ? EnumSet.noneOf(Feature.class) + : EnumSet.copyOf(Arrays.asList(lexerFeatures)); + } + + Dialect(Feature... lexerFeatures) { + this(AdjacentStringLiterals.OFF, lexerFeatures); + } + + /** Returns the immutable lexer defaults shared by this dialect preset. */ + public Set getLexerFeatures() { + return Collections.unmodifiableSet(lexerFeatures); + } + + public AdjacentStringLiterals getAdjacentStringLiterals() { + return adjacentStringLiterals; + } } public P withSquareBracketQuotation() { @@ -57,12 +106,58 @@ public P withUnsupportedStatements(boolean allowUnsupportedStatements) { return withFeature(Feature.allowUnsupportedStatements, allowUnsupportedStatements); } + /** Enables GROUP BY ASC/DESC for MySQL versions before 8.0.13. Requires MYSQL dialect. */ + public P withLegacyMySqlGroupBy(boolean enabled) { + return withFeature(Feature.allowLegacyMySqlGroupBy, enabled); + } + + /** + * Resolves MySQL executable comments against this server version (for example 90702 for 9.7.2). + * Requires the MYSQL dialect. Rendered SQL reflects this version, without conditional wrappers. + * Without an explicit version, conditional executable comments raise a lexical error. + * + * @param version major * 10000 + minor * 100 + patch, from 0 through 999999 + * @return this parser + */ + public P withMySqlServerVersion(int version) { + if (version < 0 || version > 999999) { + throw new IllegalArgumentException("MySQL server version must be between 0 and 999999"); + } + return withFeature(Feature.mySqlServerVersion, (long) version); + } + public P withTimeOut(long timeOutMillSeconds) { return withFeature(Feature.timeOut, timeOutMillSeconds); } + /** + * Applies this dialect's enabled lexer defaults to the current configuration. Existing options + * remain set; apply explicit overrides after this call, or use a new parser for a fresh preset. + */ public P withDialect(Dialect dialect) { - return withFeature(Feature.dialect, dialect.name()); + withFeature(Feature.dialect, dialect.name()); + if (dialect.getAdjacentStringLiterals() != AdjacentStringLiterals.OFF) { + withAdjacentStringLiterals(dialect.getAdjacentStringLiterals()); + } + for (Feature lexerFeature : dialect.getLexerFeatures()) { + withFeature(lexerFeature, true); + } + return me(); + } + + public P withAdjacentStringLiterals() { + return withAdjacentStringLiterals(AdjacentStringLiterals.NEWLINE); + } + + public P withAdjacentStringLiterals(boolean adjacentStringLiterals) { + return withAdjacentStringLiterals( + adjacentStringLiterals ? AdjacentStringLiterals.NEWLINE + : AdjacentStringLiterals.OFF); + } + + public P withAdjacentStringLiterals(AdjacentStringLiterals adjacentStringLiterals) { + getConfiguration().setValue(Feature.adjacentStringLiterals, adjacentStringLiterals); + return me(); } public P withAllowedNestingDepth(int allowedNestingDepth) { @@ -77,6 +172,43 @@ public P withBackslashEscapeCharacter(boolean allowBackslashEscapeCharacter) { return withFeature(Feature.allowBackslashEscapeCharacter, allowBackslashEscapeCharacter); } + /** + * Controls tagged dollar quotes; disabled by default, enabled by the PostgreSQL dialect preset. + * False preserves dollar-containing identifier spellings. + */ + public P withDollarQuotedStringTags(boolean allowDollarQuotedStringTags) { + return withFeature(Feature.allowDollarQuotedStringTags, allowDollarQuotedStringTags); + } + + public P withDoubleQuotedStrings() { + return withFeature(Feature.allowDoubleQuotedStrings, true); + } + + public P withDoubleQuotedStrings(boolean allowDoubleQuotedStrings) { + return withFeature(Feature.allowDoubleQuotedStrings, allowDoubleQuotedStrings); + } + + public P withHashLineComments() { + return withFeature(Feature.allowHashLineComments, true); + } + + public P withHashLineComments(boolean allowHashLineComments) { + return withFeature(Feature.allowHashLineComments, allowHashLineComments); + } + + public P withBlankLinesAsSeparator(boolean allowBlankLinesAsStatementSeparator) { + return withFeature(Feature.allowBlankLinesAsStatementSeparator, + allowBlankLinesAsStatementSeparator); + } + + public P withDigitSeparators() { + return withFeature(Feature.allowDigitSeparators, true); + } + + public P withDigitSeparators(boolean allowDigitSeparators) { + return withFeature(Feature.allowDigitSeparators, allowDigitSeparators); + } + public P withUnparenthesizedSubSelects() { return withFeature(Feature.allowUnparenthesizedSubSelects, true); } @@ -128,6 +260,30 @@ public Integer getAsInteger(Feature f) { return getConfiguration().getAsInteger(f); } + /** + * The INSERT OR conflict algorithms other than REPLACE are SQLite syntax. + */ + protected UpsertType insertOrType(String algorithm) throws ParseException { + if ("REPLACE".equalsIgnoreCase(algorithm)) { + return UpsertType.INSERT_OR_REPLACE; + } + if (!Dialect.SQLITE.name().equals(getAsString(Feature.dialect))) { + throw new ParseException("INSERT OR " + algorithm + " needs Dialect.SQLITE"); + } + switch (algorithm.toUpperCase()) { + case "IGNORE": + return UpsertType.INSERT_OR_IGNORE; + case "ROLLBACK": + return UpsertType.INSERT_OR_ROLLBACK; + case "ABORT": + return UpsertType.INSERT_OR_ABORT; + case "FAIL": + return UpsertType.INSERT_OR_FAIL; + default: + throw new ParseException("Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK"); + } + } + public String getAsString(Feature f) { return getConfiguration().getAsString(f); } diff --git a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java index 31e0709758..f3bbdc7ac0 100644 --- a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java +++ b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java @@ -14,6 +14,8 @@ import java.io.Reader; import java.util.Stack; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -30,6 +32,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.table.ColDataType; /** * Toolfunctions to start and use JSqlParser. @@ -59,6 +62,13 @@ public static Statement parse(Reader statementReader) throws JSQLParserException return statement; } + /** + * Parses a single SQL statement. + * + * @param sql the SQL statement to parse + * @return the parsed statement + * @throws JSQLParserException if the input is null, empty, or cannot be parsed + */ public static Statement parse(String sql) throws JSQLParserException { return parse(sql, null); } @@ -72,17 +82,15 @@ public static Statement parse(String sql) throws JSQLParserException { * CCJSqlParserUtil.parse("select * from [mytable]", parser -> parser.withSquareBracketQuotation(true)); * } * - * @param sql - * @param consumer - * @return - * @throws JSQLParserException + * @param sql the SQL statement to parse + * @param consumer parser configuration callback, or {@code null} + * @return the parsed statement + * @throws JSQLParserException if the input is null, empty, or cannot be parsed */ public static Statement parse(String sql, Consumer consumer) throws JSQLParserException { - if (sql == null || sql.isEmpty()) { - return null; - } + requireStatementInput(sql); ExecutorService executorService = Executors.newSingleThreadExecutor(); Statement statement; @@ -96,12 +104,19 @@ public static Statement parse(String sql, Consumer consumer) return statement; } + /** + * Parses a single SQL statement using the caller's executor, which is left open. + * + * @param sql the SQL statement to parse + * @param executorService executor to use for parsing + * @param consumer parser configuration callback, or {@code null} + * @return the parsed statement + * @throws JSQLParserException if the input is null, empty, or cannot be parsed + */ public static Statement parse(String sql, ExecutorService executorService, Consumer consumer) throws JSQLParserException { - if (sql == null || sql.isEmpty()) { - return null; - } + requireStatementInput(sql); Statement statement; // first, try to parse fast and simple @@ -116,6 +131,9 @@ public static Statement parse(String sql, ExecutorService executorService, LOGGER.info("Trying SIMPLE parsing " + (allowComplex ? "first" : "only")); statement = parseStatement(parser.withAllowComplexParsing(false), executorService); } catch (JSQLParserException ex) { + if (wasExecutionStopped(ex)) { + throw ex; + } LOGGER.info("Nesting Depth" + getNestingDepth(sql)); if (allowComplex && (allowedNestingDepth < 0 || getNestingDepth(sql) <= allowedNestingDepth)) { @@ -133,6 +151,12 @@ public static Statement parse(String sql, ExecutorService executorService, return statement; } + private static void requireStatementInput(String sql) throws JSQLParserException { + if (sql == null || sql.isEmpty()) { + throw new JSQLParserException("SQL statement must not be null or empty."); + } + } + public static CCJSqlParser newParser(String sql) { if (sql == null || sql.isEmpty()) { return null; @@ -199,50 +223,92 @@ public static Expression parseExpression(String expression, boolean allowPartial }); } - @SuppressWarnings("PMD.CyclomaticComplexity") + /** + * Parses a column data type fragment. The complete input must represent the data type; trailing + * tokens are rejected. + * + * @param columnDataType the column data type fragment to parse + * @return the parsed column data type, or {@code null} for a null or empty input + * @throws JSQLParserException when the input cannot be parsed completely + * @see #parseColDataType(String, Consumer) + */ + public static ColDataType parseColDataType(String columnDataType) throws JSQLParserException { + return parseColDataType(columnDataType, null); + } + + /** + * Parses a column data type fragment while allowing the parser to be configured. The complete + * input must represent the data type; trailing tokens are rejected. + * + * @param columnDataType the column data type fragment to parse + * @param consumer parser configuration callback, or {@code null} + * @return the parsed column data type, or {@code null} for a null or empty input + * @throws JSQLParserException when the input cannot be parsed completely + */ + public static ColDataType parseColDataType(String columnDataType, + Consumer consumer) throws JSQLParserException { + if (columnDataType == null || columnDataType.isEmpty()) { + return null; + } + + try { + return parseColDataType(columnDataType, false, consumer); + } catch (JSQLParserException ex) { + return parseColDataType(columnDataType, true, consumer); + } + } + + private static ColDataType parseColDataType(String columnDataType, boolean allowComplexParsing, + Consumer consumer) throws JSQLParserException { + CCJSqlParser parser = newParser(columnDataType) + .withAllowComplexParsing(allowComplexParsing); + if (consumer != null) { + consumer.accept(parser); + } + + try { + ColDataType result = parser.ColDataType(); + if (parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { + throw new JSQLParserException( + "could only parse partial column data type " + result); + } + return result; + } catch (ParseException ex) { + throw new JSQLParserException(columnDataType, ex); + } + } + public static Expression parseExpression(String expressionStr, boolean allowPartialParse, Consumer consumer) throws JSQLParserException { if (expressionStr == null || expressionStr.isEmpty()) { return null; } - Expression expression = null; - // first, try to parse fast and simple + // First require a complete simple parse, even when the caller allows a partial result. try { - CCJSqlParser parser = newParser(expressionStr).withAllowComplexParsing(false); - if (consumer != null) { - consumer.accept(parser); - } - try { - expression = parser.Expression(); - if (parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { - throw new JSQLParserException( - "could only parse partial expression " + expression.toString()); - } - } catch (ParseException 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 - // succeed - CCJSqlParser parser = newParser(expressionStr).withAllowComplexParsing(true); - if (consumer != null) { - consumer.accept(parser); - } - try { - expression = parser.Expression(); - if (!allowPartialParse - && parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { - throw new JSQLParserException( - "could only parse partial expression " + expression.toString()); - } - } catch (JSQLParserException ex) { - throw ex; - } catch (ParseException ex) { - throw new JSQLParserException(ex); + return parseExpression(expressionStr, false, false, consumer); + } catch (JSQLParserException ex) { + return parseExpression(expressionStr, true, allowPartialParse, consumer); + } + } + + private static Expression parseExpression(String expressionStr, boolean allowComplexParsing, + boolean allowPartialParse, Consumer consumer) throws JSQLParserException { + CCJSqlParser parser = newParser(expressionStr).withAllowComplexParsing(allowComplexParsing); + if (consumer != null) { + consumer.accept(parser); + } + + try { + Expression expression = parser.Expression(); + if (!allowPartialParse && parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { + throw new JSQLParserException( + "could only parse partial expression " + expression.toString()); } + return expression; + } catch (ParseException ex) { + throw new JSQLParserException(ex); } - return expression; } /** @@ -277,50 +343,9 @@ public static Expression parseCondExpression(String condExpr, boolean allowParti }); } - @SuppressWarnings("PMD.CyclomaticComplexity") public static Expression parseCondExpression(String conditionalExpressionStr, boolean allowPartialParse, Consumer consumer) throws JSQLParserException { - if (conditionalExpressionStr == null || conditionalExpressionStr.isEmpty()) { - return null; - } - - Expression expression = null; - // first, try to parse fast and simple - try { - CCJSqlParser parser = - newParser(conditionalExpressionStr).withAllowComplexParsing(false); - if (consumer != null) { - consumer.accept(parser); - } - try { - expression = parser.Expression(); - if (parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { - throw new JSQLParserException( - "could only parse partial expression " + expression.toString()); - } - } catch (ParseException ex) { - throw new JSQLParserException(ex); - } - } catch (JSQLParserException ex1) { - CCJSqlParser parser = - newParser(conditionalExpressionStr).withAllowComplexParsing(true); - if (consumer != null) { - consumer.accept(parser); - } - try { - expression = parser.Expression(); - if (!allowPartialParse - && parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { - throw new JSQLParserException( - "could only parse partial expression " + expression.toString()); - } - } catch (JSQLParserException ex) { - throw ex; - } catch (ParseException ex) { - throw new JSQLParserException(ex); - } - } - return expression; + return parseExpression(conditionalExpressionStr, allowPartialParse, consumer); } /** @@ -333,65 +358,49 @@ public static Expression parseCondExpression(String conditionalExpressionStr, public static Statement parseStatement(CCJSqlParser parser, ExecutorService executorService) throws JSQLParserException { - Statement statement; - Future future = executorService.submit(new Callable() { - @Override - public Statement call() throws ParseException { - return parser.Statement(); - } - }); - try { - statement = future.get(parser.getAsLong(Feature.timeOut), - TimeUnit.MILLISECONDS); - } catch (TimeoutException ex) { - parser.interrupted = true; - future.cancel(true); - throw new JSQLParserException("Time out occurred.", ex); - } catch (Exception ex) { - throw new JSQLParserException(ex); - } - return statement; + return executeParser(parser, executorService, parser::Statement); } /** * Parse a statement list. * - * @return the statements parsed + * @return the statements parsed, or a new empty list for null or empty input */ public static Statements parseStatements(String sqls) throws JSQLParserException { - if (sqls == null || sqls.isEmpty()) { - return null; - } - return parseStatements(sqls, null); } + /** + * Parses a statement list with optional parser configuration. + * + * @return the statements parsed, or a new empty list for null or empty input + */ public static Statements parseStatements(String sqls, Consumer consumer) throws JSQLParserException { if (sqls == null || sqls.isEmpty()) { - return null; + return new Statements(); } ExecutorService executorService = Executors.newSingleThreadExecutor(); - final Statements statements = parseStatements(sqls, executorService, consumer); - executorService.shutdown(); - - return statements; + try { + return parseStatements(sqls, executorService, consumer); + } finally { + executorService.shutdown(); + } } /** - * Parse a statement list. + * Parses a statement list using the caller's executor, which is left open. * - * @return the statements parsed + * @return the statements parsed, or a new empty list for null or empty input */ public static Statements parseStatements(String sqls, ExecutorService executorService, Consumer consumer) throws JSQLParserException { if (sqls == null || sqls.isEmpty()) { - return null; + return new Statements(); } - Statements statements = null; CCJSqlParser parser = newParser(sqls); if (consumer != null) { consumer.accept(parser); @@ -401,8 +410,11 @@ public static Statements parseStatements(String sqls, ExecutorService executorSe // first, try to parse fast and simple try { - statements = parseStatements(parser.withAllowComplexParsing(false), executorService); + return parseStatements(parser.withAllowComplexParsing(false), executorService); } catch (JSQLParserException ex) { + if (wasExecutionStopped(ex)) { + throw ex; + } // when fast simple parsing fails, try complex parsing but only if it has a chance to // succeed if (allowComplex @@ -412,10 +424,10 @@ public static Statements parseStatements(String sqls, ExecutorService executorSe if (consumer != null) { consumer.accept(parser); } - statements = parseStatements(parser.withAllowComplexParsing(true), executorService); + return parseStatements(parser.withAllowComplexParsing(true), executorService); } + throw ex; } - return statements; } /** @@ -427,24 +439,35 @@ public static Statements parseStatements(String sqls, ExecutorService executorSe */ public static Statements parseStatements(CCJSqlParser parser, ExecutorService executorService) throws JSQLParserException { - Statements statements = null; - Future future = executorService.submit(new Callable() { - @Override - public Statements call() throws ParseException { - return parser.Statements(); - } - }); + return executeParser(parser, executorService, parser::Statements); + } + + private static T executeParser(CCJSqlParser parser, ExecutorService executorService, + Callable operation) throws JSQLParserException { + Future future = executorService.submit(operation); try { - statements = future.get(parser.getAsLong(Feature.timeOut), - TimeUnit.MILLISECONDS); + return future.get(parser.getAsLong(Feature.timeOut), TimeUnit.MILLISECONDS); + } catch (InterruptedException ex) { + parser.interrupted = true; + future.cancel(true); + Thread.currentThread().interrupt(); + throw new JSQLParserException(ex); } catch (TimeoutException ex) { parser.interrupted = true; future.cancel(true); throw new JSQLParserException("Time out occurred.", ex); - } catch (Exception ex) { + } catch (CancellationException ex) { + parser.interrupted = true; + throw new JSQLParserException(ex); + } catch (ExecutionException ex) { throw new JSQLParserException(ex); } - return statements; + } + + private static boolean wasExecutionStopped(JSQLParserException exception) { + Throwable cause = exception.getCause(); + return cause instanceof InterruptedException || cause instanceof TimeoutException + || cause instanceof CancellationException; } public static void streamStatements(StatementListener listener, InputStream is, String encoding) diff --git a/src/main/java/net/sf/jsqlparser/parser/RangeNode.java b/src/main/java/net/sf/jsqlparser/parser/RangeNode.java new file mode 100644 index 0000000000..9b025a2ebc --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/parser/RangeNode.java @@ -0,0 +1,71 @@ +/*- + * #%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.parser; + +/** + * A node outside the parse tree that records the tokens an object spans. It serves an object no + * grammar rule opens a tree node for, or one whose text runs past its tree node. In that second + * case it stands in for the tree node, answering its parent, children and id, with the wider range. + */ +final class RangeNode extends Node { + private final Node treeNode; + + RangeNode(Token first, Token last, Node treeNode) { + super(CCJSqlParserTreeConstants.JJTEXPRESSION); + this.treeNode = treeNode; + jjtSetFirstToken(first); + jjtSetLastToken(last); + } + + @Override + public Node jjtGetParent() { + return treeNode == null ? null : treeNode.jjtGetParent(); + } + + @Override + public Node[] jjtGetChildren() { + return treeNode == null ? super.jjtGetChildren() : treeNode.jjtGetChildren(); + } + + @Override + public Node jjtGetChild(int i) { + return treeNode == null ? super.jjtGetChild(i) : treeNode.jjtGetChild(i); + } + + @Override + public int jjtGetNumChildren() { + return treeNode == null ? super.jjtGetNumChildren() : treeNode.jjtGetNumChildren(); + } + + @Override + public Object childrenAccept(CCJSqlParserVisitor visitor, Object data) { + return treeNode == null ? super.childrenAccept(visitor, data) + : treeNode.childrenAccept(visitor, data); + } + + @Override + public int getId() { + return treeNode == null ? super.getId() : treeNode.getId(); + } + + @Override + public String toString() { + return treeNode == null ? super.toString() : treeNode.toString(); + } + + @Override + public void dump(String prefix) { + if (treeNode == null) { + super.dump(prefix); + } else { + treeNode.dump(prefix); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java new file mode 100644 index 0000000000..c5ef37b5d0 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java @@ -0,0 +1,295 @@ +/*- + * #%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.parser; + +import static net.sf.jsqlparser.parser.CCJSqlParserConstants.*; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Finds the boundary of an opaque routine without interpreting its statements. In particular, + * expression CASE and procedural END qualifiers must not close an enclosing BEGIN block. + */ +final class RoutineBodyBoundary { + private enum Block { + BEGIN, IF, CASE_STATEMENT, CASE_EXPRESSION, LOOP, WHILE, REPEAT + } + + private final Deque blocks = new ArrayDeque<>(); + private int parentheses; + private int previousKind = -1; + private int statementKind = -1; + private boolean hasParameters; + private boolean standardReturns; + private boolean returnTypeName; + private boolean afterDot; + private boolean declarations; + private boolean bodyStarted; + private boolean compoundBody; + private boolean quotedBody; + private boolean completed; + private boolean handlerAction; + private boolean statementStart = true; + + /** + * Tracks a compound statement without a routine header, such as a MySQL trigger body. Its + * tokens belong to the body from the start, as after a routine's parameter list. + */ + static RoutineBodyBoundary forCompoundStatement() { + RoutineBodyBoundary boundary = new RoutineBodyBoundary(); + boundary.hasParameters = true; + return boundary; + } + + boolean endsAt(Token token) throws ParseException { + if (token.kind == EOF) { + if (!blocks.isEmpty()) { + throw new ParseException("Unterminated routine block"); + } + return true; + } + return token.kind == ST_SEMICOLON && parentheses == 0 && blocks.isEmpty() + && !declarations && (hasParameters || quotedBody || completed); + } + + /** + * Whether the routine header, including a return type, is complete and the body has not + * started, which is where MySQL routine characteristics appear. + */ + boolean isBeforeBody() { + return hasParameters && parentheses == 0 && !bodyStarted && !returnTypeName + && !declarations && !quotedBody && !completed; + } + + boolean startsQuotedBody(Token token, Token next) { + return parentheses == 0 && !compoundBody && !quotedBody && token.kind == K_AS + && next.kind == S_CHAR_LITERAL + && (!bodyStarted || statementKind == K_RETURN || statementKind == K_SET); + } + + void quotedBody() { + quotedBody = true; + bodyStarted = true; + declarations = false; + } + + void accept(Token token, Token next) { + int kind = token.kind; + if (kind == OPENING_BRACKET) { + parentheses++; + } else if (kind == CLOSING_BRACKET) { + if (--parentheses == 0 && !bodyStarted) { + hasParameters = true; + } + } else if (parentheses == 0 && !quotedBody && !completed) { + acceptTopLevel(token, next); + } + previousKind = kind; + afterDot = ".".equals(token.image); + } + + private void acceptTopLevel(Token token, Token next) { + if (afterDot) { + return; // Qualified type/column names may use otherwise significant keywords. + } + // END IF/CASE/LOOP/WHILE/REPEAT is one terminator, not another opener. + if (previousKind == K_END && isEndQualifier(token)) { + return; + } + if (!acceptRoutineHeader(token)) { + acceptBodyToken(token, next); + } + } + + private boolean acceptRoutineHeader(Token token) { + int kind = token.kind; + if (kind == K_RETURNS && !bodyStarted) { + standardReturns = true; + returnTypeName = true; + return true; + } + if (returnTypeName) { + returnTypeName = keyword(token, "SETOF"); + return true; + } + if (!compoundBody && (kind == K_AS || kind == K_IS) + && (!bodyStarted || statementKind == K_RETURN && !standardReturns + || statementKind == K_SET)) { + declarations = true; + bodyStarted = false; + return true; + } + return false; + } + + private void acceptBodyToken(Token token, Token next) { + int kind = token.kind; + if (acceptStatementBoundary(kind)) { + return; + } + if (!(hasParameters || declarations || bodyStarted)) { + return; + } + if (handlerAction && isSimpleStatementStart(kind)) { + startStatement(); + } + if (acceptBeginOrCase(kind) || declarations || acceptControlFlow(token)) { + return; + } + acceptStatementToken(token, next); + } + + private boolean acceptStatementBoundary(int kind) { + if (kind == ST_SEMICOLON) { + startStatement(); + return true; + } + if (kind == K_END) { + if (!blocks.isEmpty()) { + Block closed = blocks.pop(); + completed = blocks.isEmpty() && closed != Block.CASE_EXPRESSION; + } else { + // Retain legacy opaque declarations without a parameter list or BEGIN. + completed = true; + } + statementStart = false; + return true; + } + return false; + } + + private boolean acceptBeginOrCase(int kind) { + if (kind == K_BEGIN + && (!bodyStarted || statementStart || statementKind == K_DECLARE)) { + blocks.push(Block.BEGIN); + declarations = false; + compoundBody = true; + bodyStarted = true; + startStatement(); + return true; + } + if (kind == K_CASE) { + boolean statementCase = !declarations && (statementStart || handlerAction); + blocks.push(statementCase ? Block.CASE_STATEMENT : Block.CASE_EXPRESSION); + if (statementCase) { + bodyStarted = true; + compoundBody = true; + handlerAction = false; + } + statementStart = false; + return true; + } + return false; + } + + private boolean acceptControlFlow(Token token) { + int kind = token.kind; + Block block = controlBlock(token); + if ((statementStart || handlerAction) && block != null) { + blocks.push(block); + bodyStarted = true; + compoundBody = true; + handlerAction = false; + statementKind = kind; + statementStart = block == Block.LOOP || block == Block.REPEAT; + return true; + } + if (acceptOracleLoop(kind)) { + return true; + } + if (isStatementBranch(kind)) { + startStatement(); + return true; + } + return false; + } + + private boolean acceptOracleLoop(int kind) { + if (kind == K_LOOP && (statementKind == K_FOR || blocks.peek() == Block.WHILE)) { + // Oracle FOR/WHILE ... LOOP shares the same END LOOP boundary. + if (blocks.peek() != Block.WHILE) { + blocks.push(Block.LOOP); + } + startStatement(); + return true; + } + return false; + } + + private boolean isStatementBranch(int kind) { + return (kind == K_THEN || kind == K_ELSE) && !blocks.isEmpty() + && blocks.peek() != Block.CASE_EXPRESSION + || kind == K_DO && blocks.peek() == Block.WHILE; + } + + private void acceptStatementToken(Token token, Token next) { + int kind = token.kind; + if (isStatementPrefix(token, next)) { + return; + } + if (!bodyStarted) { + // SET may also be a PostgreSQL header option; AS remains recognizable above. + // CHARACTER SET and CHAR SET belong to a MySQL return type. + boolean characterSet = + kind == K_SET && (previousKind == K_CHARACTER || previousKind == K_CHAR); + if (!isSimpleStatementStart(kind) || characterSet) { + return; + } + bodyStarted = true; + } + if (statementStart) { + statementKind = kind; + } + if (statementKind == K_DECLARE && keyword(token, "HANDLER")) { + handlerAction = true; + } + statementStart = false; + } + + private boolean isStatementPrefix(Token token, Token next) { + // ATOMIC and a label precede the statement they qualify. + return previousKind == K_BEGIN && keyword(token, "ATOMIC") + || statementStart && (":".equals(next.image) || ":".equals(token.image)); + } + + private static boolean isEndQualifier(Token token) { + return token.kind == K_CASE || controlBlock(token) != null; + } + + private static Block controlBlock(Token token) { + if (token.kind == K_IF) { + return Block.IF; + } + if (token.kind == K_LOOP) { + return Block.LOOP; + } + if (keyword(token, "WHILE")) { + return Block.WHILE; + } + return keyword(token, "REPEAT") ? Block.REPEAT : null; + } + + private void startStatement() { + statementStart = true; + statementKind = -1; + handlerAction = false; + } + + private static boolean keyword(Token token, String keyword) { + return token.kind == S_IDENTIFIER && keyword.equalsIgnoreCase(token.image); + } + + private static boolean isSimpleStatementStart(int kind) { + return kind == K_RETURN || kind == K_SELECT || kind == K_INSERT || kind == K_UPDATE + || kind == K_DELETE || kind == K_REPLACE || kind == K_SET || kind == K_CALL + || kind == K_WITH || kind == K_DO; + } +} diff --git a/src/main/java/net/sf/jsqlparser/parser/RowPatternTokenContext.java b/src/main/java/net/sf/jsqlparser/parser/RowPatternTokenContext.java new file mode 100644 index 0000000000..f10b7c12b4 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/parser/RowPatternTokenContext.java @@ -0,0 +1,83 @@ +/*- + * #%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.parser; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Recognizes the BigQuery PATTERN lexical region even during JavaCC lookahead. The required initial + * ORDER BY (or optional PARTITION BY) distinguishes a clause from an ordinary function named + * MATCH_RECOGNIZE. Quoted tokens never change this state. + */ +final class RowPatternTokenContext implements CCJSqlParserConstants { + private final Deque frames = new ArrayDeque<>(); + private int depth; + private boolean pendingClause; + + private static final class Frame { + private final int depth; + private int phase; + private int patternDepth = -1; + private boolean pendingPattern; + private boolean patternComplete; + + private Frame(int depth) { + this.depth = depth; + } + } + + boolean isIdle() { + return frames.isEmpty() && !pendingClause; + } + + boolean isInPattern() { + return !frames.isEmpty() && frames.peek().patternDepth >= 0; + } + + void accept(Token token) { + if (token.kind == OPENING_BRACKET) { + depth++; + if (pendingClause) { + frames.push(new Frame(depth)); + pendingClause = false; + return; + } + if (!frames.isEmpty() && frames.peek().pendingPattern) { + frames.peek().patternDepth = depth; + frames.peek().pendingPattern = false; + } + } else if (token.kind == CLOSING_BRACKET) { + if (!frames.isEmpty()) { + Frame frame = frames.peek(); + if (frame.patternDepth == depth) { + frame.patternDepth = -1; + frame.patternComplete = true; + } + if (frame.depth == depth) { + frames.pop(); + } + } + depth--; + } else if (!frames.isEmpty() && depth == frames.peek().depth) { + Frame frame = frames.peek(); + if (frame.phase == 0) { + frame.phase = token.kind == K_ORDER || token.kind == K_PARTITION ? 1 : -1; + } else if (frame.phase == 1) { + frame.phase = token.kind == K_BY ? 2 : -1; + } else if (frame.phase == 2 && !frame.patternComplete) { + frame.pendingPattern = token.kind == S_IDENTIFIER + && "PATTERN".equalsIgnoreCase(token.image); + } + } + pendingClause = token.kind == S_IDENTIFIER + && "MATCH_RECOGNIZE".equalsIgnoreCase(token.image); + } +} diff --git a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java index 0e633ebbb5..5b6c5e8259 100644 --- a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java +++ b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java @@ -7,9 +7,19 @@ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 * #L% */ -/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 8.0.0 */ +/* Generated By:JavaCC: Do not edit this line by hand. SimpleCharStream.java Version 8.0.0 */ +/* + * Warning: this file is maintained by hand on top of the JavaCC template. Among other manual + * adjustments it carries the in-buffer '#' and blank line rewrites in BeginToken(), which + * Feature.allowHashLineComments and Feature.allowBlankLinesAsStatementSeparator depend on. Never + * overwrite it with a freshly generated SimpleCharStream, that would silently drop the rewrites and + * break both features. + */ package net.sf.jsqlparser.parser; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; + /** * An implementation of interface CharStream, where the stream is assumed to contain only ASCII * characters (without unicode processing). @@ -38,10 +48,29 @@ public class SimpleCharStream { protected boolean trackLineColumn = true; protected int totalCharsRead = 0; protected int absoluteTokenBegin = 0; + // line, column and absolute position of each match begun since the last positionSpecialTokens() + private int[] matchBegins = new int[24]; + private int matchBeginCount = 0; int bufsize; int available; int tokenBegin; + // MySQL `#` line comments (#2499): under the flag a token-start `#` is + // rewritten in the buffer to HASH_SUBSTITUTION, a character no other + // lexical rule starts with, so the dedicated HASH_LINE_COMMENT production + // wins the match for every `#` form (`# c`, `#c`, `#>`, ...). Rewriting + // the buffer itself (instead of synthesizing reads) keeps the matcher's + // backup / re-read arithmetic intact; the production's action restores + // the `#` in the token image. Wired (with the token manager's + // configuration) before parsing, null keeps this inert. + static final char HASH_SUBSTITUTION = '\u0001'; + static final char MYSQL_MINUS_SUBSTITUTION = '\u0002'; + static final char MYSQL_COMMENT_ERROR = '\u0003'; + FeatureConfiguration featureConfiguration; + private boolean mySqlExecutableComment; + String mySqlCommentError; + boolean mySqlMinusSubstitution; + /** * Constructor. */ @@ -81,6 +110,38 @@ public final int getAbsoluteTokenBegin() { return absoluteTokenBegin; } + /** + * Gives the comments in front of {@code token} the positions where their matches began, from + * the begins recorded since the last call, and forgets those. Comments come through no token + * action, so this is how they get a position. The comments chain backwards from the token while + * the begins are recorded forwards, so one backwards pass pairs them. + */ + public final void positionSpecialTokens(Token token) { + int i = matchBeginCount - 3; + for (Token comment = token.specialToken; comment != null; comment = comment.specialToken) { + while (i >= 0 && (matchBegins[i] != comment.beginLine + || matchBegins[i + 1] != comment.beginColumn)) { + i -= 3; + } + if (i < 0) { + break; + } + comment.absoluteBegin = matchBegins[i + 2]; + comment.absoluteEnd = comment.absoluteBegin + comment.image.length(); + i -= 3; + } + matchBeginCount = 0; + } + + private void recordMatchBegin() { + if (matchBeginCount + 3 > matchBegins.length) { + matchBegins = java.util.Arrays.copyOf(matchBegins, matchBegins.length * 2); + } + matchBegins[matchBeginCount++] = getBeginLine(); + matchBegins[matchBeginCount++] = getBeginColumn(); + matchBegins[matchBeginCount++] = absoluteTokenBegin; + } + protected void ExpandBuff(boolean wrapAround) { final int newSize = bufsize * 2; char[] newbuffer = new char[newSize]; @@ -153,15 +214,138 @@ protected void FillBuff() throws java.io.IOException { * Start. */ public final char BeginToken() throws java.io.IOException { + mySqlMinusSubstitution = false; + if (mySqlCommentError != null) { + // Parser lookahead may catch lexical errors. Keep this failure sticky until ReInit + // rather than allowing a later token request to continue after the rejected marker. + return MYSQL_COMMENT_ERROR; + } tokenBegin = -1; - char c = readChar(); + char c; + try { + c = readChar(); + } catch (java.io.IOException e) { + if (!mySqlExecutableComment) { + throw e; + } + // JavaCC treats exceptions from BeginToken as EOF. Emit a real token whose lexical + // action reports the error, so an unclosed executable comment cannot succeed as SQL. + c = mySqlCommentError("Unterminated MySQL executable comment"); + } tokenBegin = bufpos; absoluteTokenBegin = totalCharsRead; + recordMatchBegin(); + + if (c == '#' && featureConfiguration != null + && featureConfiguration.getAsBoolean(Feature.allowHashLineComments)) { + buffer[bufpos] = HASH_SUBSTITUTION; + c = HASH_SUBSTITUTION; + } else if ((c == '-' || c == '/' || c == '*') && featureConfiguration != null + && AbstractJSqlParser.Dialect.MYSQL.name() + .equals(featureConfiguration.getValue(Feature.dialect))) { + c = mySqlCommentBoundary(c); + } else if (c == '\n' && featureConfiguration != null + && !featureConfiguration + .getAsBoolean(Feature.allowBlankLinesAsStatementSeparator) + && peekCharacters(2).equals("\n\n")) { + // Blanking the first newline of the run stops ST_SEMICOLON matching "\n\n\n"; + // line and column tracking was already updated when the newline was read. + buffer[bufpos] = ' '; + c = ' '; + } + return c; + } + /** Changes only token boundaries, never characters being scanned inside strings or names. */ + private char mySqlCommentBoundary(char c) throws java.io.IOException { + if (c == '-') { + String following = peekCharacters(2); + if (following.length() == 2 && following.charAt(0) == '-' + && !isMySqlCommentSpace(following.charAt(1))) { + mySqlMinusSubstitution = true; + buffer[bufpos] = MYSQL_MINUS_SUBSTITUTION; + return MYSQL_MINUS_SUBSTITUTION; + } + } else if (c == '*' && mySqlExecutableComment && peekCharacters(1).equals("/")) { + eraseCommentMarker(2); + mySqlExecutableComment = false; + return ' '; + } else if (c == '/') { + String following = peekCharacters(9); + if (following.startsWith("*!")) { + if (mySqlExecutableComment) { + return mySqlCommentError("Nested MySQL executable comments are not supported"); + } + int digits = mySqlCommentVersionLength(following); + if (digits > 0) { + long target = featureConfiguration.getAsLong(Feature.mySqlServerVersion); + if (target < 0 || target > 999999) { + return mySqlCommentError("MySQL conditional executable comments require " + + "withMySqlServerVersion(major * 10000 + minor * 100 + patch)"); + } + int minimum = Integer.parseInt(following.substring(2, 2 + digits)); + if (minimum > target) { + return c; + } + } + eraseCommentMarker(3 + digits); + mySqlExecutableComment = true; + return ' '; + } + } return c; } + private static boolean isMySqlCommentSpace(char c) { + return c <= ' ' || c == '\u007f'; + } + + private static int mySqlCommentVersionLength(String following) { + if (following.length() < 7) { + return 0; + } + for (int i = 2; i < 7; i++) { + if (following.charAt(i) < '0' || following.charAt(i) > '9') { + return 0; + } + } + // MySQL recognizes a sixth digit only when followed by whitespace. Otherwise the + // five-digit prefix is the version and the remaining characters belong to the SQL body. + return following.length() >= 9 && following.charAt(7) >= '0' + && following.charAt(7) <= '9' && (following.charAt(8) == ' ' + || following.charAt(8) >= '\t' && following.charAt(8) <= '\r') + ? 6 + : 5; + } + + private String peekCharacters(int length) { + StringBuilder result = new StringBuilder(length); + try { + for (int i = 0; i < length; i++) { + result.append(readChar()); + } + } catch (java.io.IOException ignored) { + // Only the successfully read characters need to be rewound, including at EOF. + } + backup(result.length()); + return result.toString(); + } + + private void eraseCommentMarker(int length) throws java.io.IOException { + buffer[bufpos] = ' '; + for (int i = 1; i < length; i++) { + readChar(); + buffer[bufpos] = ' '; + } + backup(length - 1); + } + + private char mySqlCommentError(String message) { + mySqlCommentError = message; + return MYSQL_COMMENT_ERROR; + } + protected final void UpdateLineColumn(char c) { column++; @@ -293,6 +477,7 @@ public final void backup(int amount) { * Reinitialise. */ public void ReInit(Provider dstream, int startline, int startcolumn, int buffersize) { + matchBeginCount = 0; inputStream = dstream; line = startline; column = startcolumn - 1; @@ -305,6 +490,8 @@ public void ReInit(Provider dstream, int startline, int startcolumn, int buffers prevCharIsLF = prevCharIsCR = false; tokenBegin = inBuf = maxNextCharInd = 0; bufpos = -1; + mySqlExecutableComment = false; + mySqlCommentError = null; } /** @@ -325,6 +512,25 @@ public void ReInit(Provider dstream) { * Get token literal value. */ public String GetImage() { + // restore the `#` rewritten to HASH_SUBSTITUTION: only the comment + // token's own window starts with the sentinel (token-start rewrites + // only), so this is exact and covers every consumer of GetImage. + // The wiring check keeps parses that never opted in on the original + // cost (the stream is only wired through the feature consumers / + // withConfiguration) + if (featureConfiguration == null) { + return doGetImage(); + } + String image = doGetImage(); + if (!image.isEmpty() && image.charAt(0) == HASH_SUBSTITUTION) { + image = "#" + image.substring(1); + } else if (!image.isEmpty() && image.charAt(0) == MYSQL_MINUS_SUBSTITUTION) { + image = "-" + image.substring(1); + } + return image; + } + + private String doGetImage() { if (bufpos >= tokenBegin) { return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); } else { 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 d786f5170a..3ce49efd6a 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -15,6 +15,7 @@ import net.sf.jsqlparser.expression.OracleHierarchicalExpression; import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; +import net.sf.jsqlparser.parser.AbstractJSqlParser; import net.sf.jsqlparser.statement.Block; import net.sf.jsqlparser.statement.Commit; import net.sf.jsqlparser.statement.CreateFunctionalStatement; @@ -25,11 +26,14 @@ import net.sf.jsqlparser.statement.SetStatement; import net.sf.jsqlparser.statement.ShowColumnsStatement; import net.sf.jsqlparser.statement.ShowStatement; +import net.sf.jsqlparser.statement.TableMaintenanceStatement; import net.sf.jsqlparser.statement.UseStatement; import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; 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; @@ -37,6 +41,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -45,6 +50,8 @@ import net.sf.jsqlparser.statement.grant.Grant; import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.role.ReassignOwned; import net.sf.jsqlparser.statement.select.Fetch; import net.sf.jsqlparser.statement.select.First; import net.sf.jsqlparser.statement.select.KSQLWindow; @@ -57,6 +64,7 @@ import net.sf.jsqlparser.statement.select.TableFunction; import net.sf.jsqlparser.statement.select.Top; import net.sf.jsqlparser.statement.select.UnPivot; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.show.*; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.Update; @@ -69,10 +77,16 @@ public enum Feature { * "SELECT" */ select, + /** Structured FROM row pattern recognition. */ + matchRecognize, + /** BigQuery longest-match option. */ + matchRecognizeOptions, /** * "GROUP BY" */ selectGroupBy, + /** Explicit ASC/DESC on GROUP BY items in legacy MySQL. */ + selectGroupByOrdering, /** * "GROUPING SETS" */ @@ -178,6 +192,10 @@ public enum Feature { * "SEMI" join */ joinSemi, + /** + * @see net.sf.jsqlparser.statement.select.Join#isAnti() + */ + joinAnti, /** * "INNER" join */ @@ -191,6 +209,9 @@ public enum Feature { */ joinApply, + /** Nearest-match ASOF join. */ + joinAsOf, + joinWindow, joinUsingColumns, /** @@ -333,6 +354,9 @@ public enum Feature { * @see Update */ update, + + /** Application-time FOR PORTION OF in UPDATE and DELETE. */ + forPortion, /** * "UPDATE table1 SET ... FROM table2 */ @@ -432,6 +456,19 @@ public enum Feature { */ alterIndex, + /** + * MySQL and PostgreSQL "CREATE TABLESPACE" statement is allowed + * + * @see net.sf.jsqlparser.statement.create.tablespace.CreateTablespace + */ + createTablespace, + /** + * MySQL and PostgreSQL "ALTER TABLESPACE" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterTablespace + */ + alterTablespace, + /** * SQL "ANALYZE" statement is allowed @@ -440,6 +477,14 @@ public enum Feature { */ analyze, + /** + * MySQL/MariaDB "CHECK TABLE", "CHECKSUM TABLE", "OPTIMIZE TABLE" and "REPAIR TABLE" statements + * are allowed + * + * @see TableMaintenanceStatement + */ + tableMaintenance, + /** * SQL "TRUNCATE" statement is allowed * @@ -451,7 +496,9 @@ public enum Feature { * * @see Execute */ - execute, executeExec, executeCall, executeExecute, + oracleBlock, + + execute, executeExec, executeCall, executeExecute, doStatement, insertBulk, /** * SQL "EXECUTE" statement is allowed @@ -474,13 +521,33 @@ public enum Feature { * @see Drop */ drop, dropTable, dropIndex, dropView, dropSchema, dropSequence, dropTableIfExists, dropIndexIfExists, dropViewIfExists, dropSchemaIfExists, dropSequenceIfExists, + /** + * PostgreSQL "REASSIGN OWNED BY ... TO ..." statement is allowed + * + * @see ReassignOwned + */ + reassignOwned, + /** + * SQL "CREATE DATABASE" statement is allowed + * + * @see CreateDatabase + */ + createDatabase, + /** + * SQL "ALTER DATABASE" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.database.AlterDatabase + */ + alterDatabase, /** * SQL "CREATE SCHEMA" statement is allowed * * @see CreateSchema */ createSchema, + /** SQL ALTER SCHEMA rename and owner changes. */ + alterSchema, /** * SQL "CREATE VIEW" statement is allowed * @@ -550,7 +617,17 @@ public enum Feature { * * @see CreateSequence */ - createSequence, + createSequence, createAccessMethod, createAccessMethodTable, + /** Publication and subscription definitions. */ + createForeignDataWrapper, alterForeignDataWrapper, createServer, alterServer, createUserMapping, alterUserMapping, + + createRule, notifyStatement, createTextSearchConfiguration, alterTextSearchConfiguration, createCollation, alterCollation, + + createPublication, alterPublication, createSubscription, alterSubscription, + /** + * Structured type, domain and extension statements. + */ + createType, alterType, createDomain, alterDomain, createExtension, alterExtension, /** * SQL "CREATE SYNONYM" statement is allowed * @@ -561,12 +638,44 @@ public enum Feature { * SQL "CREATE TRIGGER" statement is allowed */ createTrigger, + /** + * PostgreSQL "CREATE EVENT TRIGGER" and "ALTER EVENT TRIGGER" statements are allowed + * + * @see CreateEventTrigger + * @see AlterEventTrigger + */ + createEventTrigger, alterEventTrigger, + /** + * PostgreSQL "REINDEX" statement is allowed; "REINDEX … CONCURRENTLY" additionally requires + * {@link #reindexConcurrently} + * + * @see Reindex + */ + reindex, reindexConcurrently, + /** + * PostgreSQL "SECURITY LABEL" statement is allowed + * + * @see SecurityLabel + */ + securityLabel, /** * SQL "COMMIT" statement is allowed * * @see Commit */ commit, + /** START TRANSACTION and dialect-specific BEGIN transaction statements. */ + startTransaction, + /** RELEASE [SAVEPOINT] statements. */ + releaseSavepoint, + /** Explicit AND [NO] CHAIN transaction completion. */ + transactionChain, + /** Isolation level specified directly on BEGIN or START TRANSACTION. */ + transactionIsolationLevel, + /** PostgreSQL [NOT] DEFERRABLE transaction characteristic. */ + transactionDeferrable, + /** MySQL WITH CONSISTENT SNAPSHOT transaction characteristic. */ + transactionConsistentSnapshot, /** * SQL "COMMENT ON" statement is allowed * @@ -586,6 +695,36 @@ public enum Feature { */ commentOnView, + /** Additional structured catalog targets of COMMENT ON. */ + commentOnIndex, commentOnSchema, commentOnSequence, commentOnDomain, commentOnType, commentOnMaterializedView, commentOnFunction, commentOnConstraint, + + /** COMMENT ON TRIGGER, POLICY or RULE within a table. */ + commentOnTrigger, commentOnPolicy, commentOnRule, + + /** COMMENT ON PROCEDURE, ROUTINE, AGGREGATE or CAST. */ + commentOnProcedure, commentOnRoutine, commentOnAggregate, commentOnCast, + + /** COMMENT ON COLLATION, FOREIGN TABLE or STATISTICS. */ + commentOnCollation, commentOnForeignTable, commentOnStatistics, + + /** COMMENT ON DATABASE, ROLE or TABLESPACE. */ + commentOnDatabase, commentOnRole, commentOnTablespace, + + /** COMMENT ON ACCESS METHOD, EXTENSION, FOREIGN DATA WRAPPER, LANGUAGE or SERVER. */ + commentOnAccessMethod, commentOnExtension, commentOnForeignDataWrapper, commentOnLanguage, commentOnServer, + + /** COMMENT ON EVENT TRIGGER, PUBLICATION or SUBSCRIPTION. */ + commentOnEventTrigger, commentOnPublication, commentOnSubscription, + + /** COMMENT ON OPERATOR, OPERATOR CLASS or OPERATOR FAMILY. */ + commentOnOperator, commentOnOperatorClass, commentOnOperatorFamily, + + /** COMMENT ON CONVERSION, LARGE OBJECT or TRANSFORM. */ + commentOnConversion, commentOnLargeObject, commentOnTransform, + + /** COMMENT ON TEXT SEARCH CONFIGURATION, DICTIONARY, PARSER or TEMPLATE. */ + commentOnTextSearchConfiguration, commentOnTextSearchDictionary, commentOnTextSearchParser, commentOnTextSearchTemplate, + /** * SQL "DESCRIBE" statement is allowed * @@ -622,14 +761,39 @@ public enum Feature { * @see ShowIndexStatement */ showIndex, + /** + * MySQL/MariaDB "SHOW TABLE STATUS" statement is allowed + * + * @see ShowTableStatusStatement + */ + showTableStatus, + /** + * MySQL/MariaDB "SHOW CREATE" statement is allowed + * + * @see ShowCreateStatement + */ + showCreate, /** * @see UseStatement */ use, + /** SQL Server session permission to insert explicit identity values into a table. */ + setIdentityInsert, /** * @see Grant */ - grant, + grant, revoke, createRole, alterRole, alterDefaultPrivileges, + /** + * MySQL/MariaDB account management: "ALTER USER", "RENAME USER", "SET ROLE", "SET DEFAULT ROLE" + * and "SET PASSWORD" + * + * @see net.sf.jsqlparser.statement.alter.AlterUser + * @see net.sf.jsqlparser.statement.alter.RenameUser + * @see net.sf.jsqlparser.statement.SetRoleStatement + * @see net.sf.jsqlparser.statement.SetDefaultRoleStatement + * @see net.sf.jsqlparser.statement.SetPasswordStatement + */ + alterUser, renameUser, setRole, setDefaultRole, setPassword, /** * @see Function */ @@ -646,6 +810,20 @@ public enum Feature { * @see CreateFunctionalStatement */ functionalStatement, + + alterFunction, alterProcedure, createOrAlterRoutine, + /** + * PostgreSQL "ALTER ROUTINE" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterRoutine + */ + alterRoutine, + /** + * PostgreSQL "ALTER TRIGGER" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterTrigger + */ + alterTrigger, /** * SQL block starting with "BEGIN" and ends with "END" statement is allowed * @@ -656,10 +834,17 @@ public enum Feature { * @see DeclareStatement */ declare, + + /** SQL Server local table variables in queries and DML targets. */ + tableVariable, /** * @see SetStatement */ set, + /** PostgreSQL SET configuration_parameter TO value. */ + setAssignmentTo, + /** SQL Server SET option [, option] ON | OFF. */ + sqlServerSetOptions, /** * @see ResetStatement */ @@ -748,7 +933,7 @@ public enum Feature { // MYSQL - mySqlHintStraightJoin, mysqlSqlCacheFlag, mysqlCalcFoundRows, + mySqlHintStraightJoin, mysqlSqlCacheFlag, mysqlCalcFoundRows, mysqlSelectModifiers, // SQLSERVER @@ -767,6 +952,17 @@ public enum Feature { */ allowPostgresSpecificSyntax(false), + /** Enables legacy GROUP BY ordering with the MYSQL dialect; disabled by default. */ + allowLegacyMySqlGroupBy(false), + + /** + * Target MySQL version for executable comments, encoded as major * 10000 + minor * 100 + patch. + * A negative value leaves the version unspecified: conditional comments then fail instead of + * silently discarding SQL. Only used with the MYSQL dialect; unversioned comments need no + * value. + */ + mySqlServerVersion(-1), + // PERFORMANCE /** @@ -788,6 +984,45 @@ public enum Feature { */ allowBackslashEscapeCharacter(false), + /** + * allows double quoted String Literals (BigQuery, Spark/Databricks, MySQL default sql_mode); + * disabled by default, where double quotes stay quoted identifiers (ANSI SQL) + */ + allowDoubleQuotedStrings(false), + + /** + * Recognizes PostgreSQL $tag$...$tag$ literals; disabled by default to preserve unquoted + * identifiers, enabled by the PostgreSQL dialect preset. Untagged $$ literals are unaffected. + */ + allowDollarQuotedStringTags(false), + + /** + * concatenates adjacent String Literals: NEWLINE when separated by whitespace with at least one + * newline (the SQL standard and PostgreSQL), WHITESPACE across any whitespace (GoogleSQL, + * Spark/Databricks); OFF by default, where the second literal stays an alias (MySQL, SQL + * Server) or fails (everywhere else) + */ + adjacentStringLiterals(AbstractJSqlParser.AdjacentStringLiterals.OFF), + + /** + * allows MySQL `#` line comments; disabled by default, where a lone `#` stays the binary + * operator (#2507: PostgreSQL bitwise XOR / geometric intersection) + */ + allowHashLineComments(false), + + /** + * treats three consecutive newlines as a statement separator (enabled by default). When + * disabled, blank lines are plain whitespace, so a statement containing them is parsed whole + * instead of ending, or failing to parse, at the first run of blank lines + */ + allowBlankLinesAsStatementSeparator(true), + + /** + * allows digit separators in numeric literals, e.g. {@code 1_000_000} (DuckDB, PostgreSQL 16); + * disabled by default, where MySQL reads {@code 2024_01} as an identifier + */ + allowDigitSeparators(false), + /** * allows sub selects without parentheses, e.g. `select * from dual where 1 = select 1` */ diff --git a/src/main/java/net/sf/jsqlparser/schema/Column.java b/src/main/java/net/sf/jsqlparser/schema/Column.java index 33240ac52a..3bb06de241 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Column.java +++ b/src/main/java/net/sf/jsqlparser/schema/Column.java @@ -49,10 +49,14 @@ public Column(List nameParts) { } public Column(List nameParts, List delimiters) { - this( - nameParts.size() > 1 ? new Table(nameParts.subList(0, nameParts.size() - 1), - delimiters.subList(0, delimiters.size() - 1)) : null, - nameParts.get(nameParts.size() - 1)); + if (nameParts.size() > 1) { + setTable(new Table(nameParts.subList(0, nameParts.size() - 1), + delimiters.subList(0, delimiters.size() - 1))); + // The final component is already separated from the table name. + setName(nameParts.get(nameParts.size() - 1), false); + } else { + setColumnName(nameParts.get(0)); + } setTableDelimiter(delimiters.isEmpty() ? "." : delimiters.get(delimiters.size() - 1)); } @@ -149,36 +153,25 @@ public void setColumnName(String name) { } 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); + if (name.contains(".") && splitNamesOnDelimiter) { + List parts = splitName(name); + if (parts.size() == 1 && MultiPartName.isQuoted(name)) { + parts.clear(); + for (String identifier : MultiPartName.unquote(name).split("\\.")) { + parts.add("\"" + identifier + "\""); + } } - } else if (name.contains(".") && splitNamesOnDelimiter) { - String[] parts = MultiPartName.unquote(name).split("\\."); - switch (parts.length) { + switch (parts.size()) { case 3: - this.table = new Table(parts[0] + "." + parts[1]); - this.columnName = parts[2]; + this.table = new Table(parts.subList(0, 2)); + this.columnName = parts.get(2); break; case 2: - this.table = new Table(parts[0]); - this.columnName = parts[1]; + this.table = new Table(parts.get(0), false); + this.columnName = parts.get(1); break; case 1: - this.columnName = parts[0]; + this.columnName = parts.get(0); break; default: throw new RuntimeException("Invalid column name: " + name); @@ -188,6 +181,34 @@ public void setName(String name, boolean splitNamesOnDelimiter) { } } + private static List splitName(String name) { + List parts = new ArrayList<>(); + int start = 0; + char quote = 0; + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + if (quote != 0) { + if (ch == quote) { + if (i + 1 < name.length() && name.charAt(i + 1) == quote) { + i++; + } else { + quote = 0; + } + } + } else if (ch == '"' || ch == '`' || ch == '[') { + quote = ch == '[' ? ']' : ch; + } else if (ch == '.') { + parts.add(name.substring(start, i)); + start = i + 1; + } + } + parts.add(name.substring(start)); + while (!parts.isEmpty() && parts.get(parts.size() - 1).isEmpty()) { + parts.remove(parts.size() - 1); + } + return parts; + } + public String getTableDelimiter() { return tableDelimiter; } diff --git a/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java b/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java index ce954780d7..b49e58384e 100644 --- a/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java +++ b/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java @@ -23,13 +23,34 @@ public interface MultiPartName { * @return the pure identifier without quotes */ static String unquote(String quotedIdentifier) { - return quotedIdentifier != null - ? LEADING_TRAILING_QUOTES_PATTERN.matcher(quotedIdentifier).replaceAll("") - : null; + if (quotedIdentifier == null) { + return null; + } + // same result as LEADING_TRAILING_QUOTES_PATTERN.matcher(..).replaceAll(""), but linear: + // the regex retries its trailing alternative at every offset of a run of quotes + int start = 0; + int end = quotedIdentifier.length(); + while (start < end && isLeadingQuote(quotedIdentifier.charAt(start))) { + start++; + } + while (end > start && isTrailingQuote(quotedIdentifier.charAt(end - 1))) { + end--; + } + return quotedIdentifier.substring(start, end); } static boolean isQuoted(String identifier) { - return identifier != null && LEADING_TRAILING_QUOTES_PATTERN.matcher(identifier).find(); + return identifier != null && !identifier.isEmpty() + && (isLeadingQuote(identifier.charAt(0)) + || isTrailingQuote(identifier.charAt(identifier.length() - 1))); + } + + private static boolean isLeadingQuote(char c) { + return c == '"' || c == '[' || c == '`'; + } + + private static boolean isTrailingQuote(char c) { + return c == '"' || c == ']' || c == '`'; } String getFullyQualifiedName(); diff --git a/src/main/java/net/sf/jsqlparser/schema/Sequence.java b/src/main/java/net/sf/jsqlparser/schema/Sequence.java index 5d7b960cdc..6125fce3af 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Sequence.java +++ b/src/main/java/net/sf/jsqlparser/schema/Sequence.java @@ -30,6 +30,15 @@ public class Sequence extends ASTNodeAccessImpl implements MultiPartName { private List parameters; private String dataType; + private SequenceOwnership ownership; + + public SequenceOwnership getOwnership() { + return ownership; + } + + public void setOwnership(SequenceOwnership ownership) { + this.ownership = ownership; + } public Sequence() {} @@ -151,6 +160,9 @@ public String toString() { sql.append(" ").append(parameter.formatParameter()); } } + if (ownership != null) { + sql.append(' ').append(ownership); + } return sql.toString(); } @@ -175,7 +187,7 @@ public Sequence addParameters(Collection parameters) { * The available parameters to a sequence */ public enum ParameterType { - INCREMENT_BY, INCREMENT, START_WITH, START, 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, NO_MINVALUE, NO_MAXVALUE, NO_CYCLE, SEQUENCE_NAME; public static ParameterType from(String type) { return Enum.valueOf(ParameterType.class, type.toUpperCase(Locale.ROOT)); @@ -185,10 +197,11 @@ public static ParameterType from(String type) { /** * Represents a parameter when declaring a sequence */ - public static class Parameter { + public static class Parameter implements java.io.Serializable { private final ParameterType option; private Long value; + private Sequence sequenceName; public Parameter(ParameterType option) { this.option = option; @@ -198,12 +211,37 @@ public Long getValue() { return value; } + public ParameterType getOption() { + return option; + } + + /** The sequence named by an identity declaration's SEQUENCE NAME option. */ + public Sequence getSequenceName() { + return sequenceName; + } + + public void setSequenceName(Sequence sequenceName) { + this.sequenceName = sequenceName; + } + + public Parameter withSequenceName(Sequence sequenceName) { + setSequenceName(sequenceName); + return this; + } + + @Override + public String toString() { + return formatParameter(); + } + public void setValue(Long value) { this.value = value; } public String formatParameter() { switch (option) { + case SEQUENCE_NAME: + return "SEQUENCE NAME " + sequenceName.getFullyQualifiedName(); case INCREMENT_BY: return prefix("INCREMENT BY"); case INCREMENT: @@ -222,6 +260,10 @@ public String formatParameter() { case MINVALUE: case CACHE: return prefix(option.name()); + case NO_MINVALUE: + case NO_MAXVALUE: + case NO_CYCLE: + return option.name().replace('_', ' '); default: // fallthrough just return option name return option.name(); diff --git a/src/main/java/net/sf/jsqlparser/schema/SequenceOwnership.java b/src/main/java/net/sf/jsqlparser/schema/SequenceOwnership.java new file mode 100644 index 0000000000..8f3f95322e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/schema/SequenceOwnership.java @@ -0,0 +1,43 @@ +/*- + * #%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.schema; + +import java.io.Serializable; +import java.util.Objects; + +/** Distinguishes explicit OWNED BY NONE from an owning column and from an omitted clause. */ +public class SequenceOwnership implements Serializable { + private final Column column; + + private SequenceOwnership(Column column) { + this.column = column; + } + + public static SequenceOwnership none() { + return new SequenceOwnership(null); + } + + public static SequenceOwnership ownedBy(Column column) { + return new SequenceOwnership(Objects.requireNonNull(column, "column")); + } + + public Column getColumn() { + return column; + } + + public boolean isNone() { + return column == null; + } + + @Override + public String toString() { + return "OWNED BY " + (column == null ? "NONE" : column); + } +} diff --git a/src/main/java/net/sf/jsqlparser/schema/Server.java b/src/main/java/net/sf/jsqlparser/schema/Server.java index 9ac9bd2d2b..7e4eaa51ca 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 1de14a8a5e..0e23b28a9c 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Table.java +++ b/src/main/java/net/sf/jsqlparser/schema/Table.java @@ -44,6 +44,8 @@ public class Table extends ASTNodeAccessImpl private List partDelimiters = new ArrayList<>(); + private boolean tableVariable; + // holds the various `time travel` syntax for BigQuery, RedShift, Snowflake or RedShift private String timeTravelStr = null; @@ -67,6 +69,20 @@ public class Table extends ASTNodeAccessImpl public Table() {} + /** Whether this reference names a local table variable rather than a catalog table. */ + public boolean isTableVariable() { + return tableVariable; + } + + public void setTableVariable(boolean tableVariable) { + this.tableVariable = tableVariable; + } + + public Table withTableVariable(boolean tableVariable) { + setTableVariable(tableVariable); + return this; + } + /** * Instantiates a new Table. * @@ -505,15 +521,16 @@ public Table getResolvedTable() { } /** - * Sets resolved table. + * Stores a detached copy of the resolved table's identifier. Reference-specific flags and + * clauses are not copied. * * @param resolvedTable the resolved table * @return this table */ public Table setResolvedTable(Table resolvedTable) { - // clone, not reference if (resolvedTable != null) { - this.resolvedTable = new Table(resolvedTable.getFullyQualifiedName()); + this.resolvedTable = new Table(); + resolvedTable.copyIdentifierPartsTo(this.resolvedTable); } return this; } @@ -527,6 +544,9 @@ public Table setResolvedTable(Table resolvedTable) { * @return the provided table */ public Table setUnsetCatalogAndSchema(String currentCatalogName, String currentSchemaName) { + if (tableVariable) { + return this; + } String databaseName = getDatabaseName(); if (databaseName == null || databaseName.isEmpty()) { setDatabaseName(currentCatalogName); @@ -558,10 +578,19 @@ public static Table[] setUnsetCatalogAndSchema(String currentCatalogName, return tables; } + /** Copies the table identifier, table-variable flag and resolved table identifier. */ @Override public Table clone() { - Table clone = new Table(this.getFullyQualifiedName()); + Table clone = new Table(); + copyIdentifierPartsTo(clone); + clone.setTableVariable(tableVariable); clone.setResolvedTable(this.resolvedTable != null ? this.resolvedTable.clone() : null); return clone; } + + /** Copies identifier components and separators, leaving the target's other state unchanged. */ + private void copyIdentifierPartsTo(Table target) { + target.partItems = new ArrayList<>(partItems); + target.partDelimiters = new ArrayList<>(partDelimiters); + } } diff --git a/src/main/java/net/sf/jsqlparser/schema/TableReference.java b/src/main/java/net/sf/jsqlparser/schema/TableReference.java new file mode 100644 index 0000000000..1584d1dc21 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/schema/TableReference.java @@ -0,0 +1,91 @@ +/*- + * #%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.schema; + +import java.io.Serializable; + +/** A relation target with optional PostgreSQL inheritance scope, shared by LOCK and TRUNCATE. */ +public class TableReference implements Serializable { + private Table table; + private boolean only; + private boolean includeDescendants; + private boolean parenthesized; + + public TableReference(Table table) { + this.table = table; + } + + public Table getTable() { + return table; + } + + public void setTable(Table table) { + this.table = table; + } + + public boolean isOnly() { + return only; + } + + public void setOnly(boolean only) { + this.only = only; + if (only) { + includeDescendants = false; + } else { + parenthesized = false; + } + } + + public boolean isIncludeDescendants() { + return includeDescendants; + } + + public void setIncludeDescendants(boolean includeDescendants) { + this.includeDescendants = includeDescendants; + if (includeDescendants) { + only = false; + parenthesized = false; + } + } + + public boolean isParenthesized() { + return parenthesized; + } + + public void setParenthesized(boolean parenthesized) { + this.parenthesized = parenthesized; + if (parenthesized) { + only = true; + includeDescendants = false; + } + } + + public StringBuilder appendTo(StringBuilder builder) { + if (only) { + builder.append("ONLY "); + } + if (parenthesized) { + builder.append('('); + } + builder.append(table.getFullyQualifiedName()); + if (parenthesized) { + builder.append(')'); + } + if (includeDescendants) { + builder.append(" *"); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java b/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java new file mode 100644 index 0000000000..58a294bb5f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java @@ -0,0 +1,91 @@ +/*- + * #%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.statement; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +import java.util.function.Consumer; + +/** + * BigQuery's {@code ASSERT expression [AS description]}, which fails the script when the expression + * does not evaluate to {@code TRUE}. + * + * @see Debugging + * statements + */ +public class AssertStatement extends ASTNodeAccessImpl implements Statement { + private Expression expression; + private StringValue description; + + public AssertStatement() {} + + public AssertStatement(Expression expression) { + this.expression = expression; + } + + public AssertStatement(Expression expression, StringValue description) { + this.expression = expression; + this.description = description; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public AssertStatement withExpression(Expression expression) { + setExpression(expression); + return this; + } + + public StringValue getDescription() { + return description; + } + + public void setDescription(StringValue description) { + this.description = description; + } + + public AssertStatement withDescription(StringValue description) { + setDescription(description); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append); + } + + /** Shares statement syntax with deparsers while visiting both expression and description. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) { + builder.append("ASSERT "); + expressionRenderer.accept(expression); + if (description != null) { + builder.append(" AS "); + expressionRenderer.accept(description); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/AttachStatement.java b/src/main/java/net/sf/jsqlparser/statement/AttachStatement.java new file mode 100644 index 0000000000..84726280d1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/AttachStatement.java @@ -0,0 +1,122 @@ +/*- + * #%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.statement; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.select.PlainSelect; + +import java.util.List; + +/** + * DuckDB's {@code ATTACH [DATABASE] [IF NOT EXISTS] path [AS alias] [(options)]}, which adds + * another database file or remote database to the session. + * + * @see ATTACH + */ +public class AttachStatement extends ASTNodeAccessImpl implements Statement { + private String databasePath; + private String alias; + private boolean usingDatabaseKeyword; + private boolean ifNotExists; + private List options; + + public String getDatabasePath() { + return databasePath; + } + + public void setDatabasePath(String databasePath) { + this.databasePath = databasePath; + } + + public AttachStatement withDatabasePath(String databasePath) { + setDatabasePath(databasePath); + return this; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public AttachStatement withAlias(String alias) { + setAlias(alias); + return this; + } + + public boolean isUsingDatabaseKeyword() { + return usingDatabaseKeyword; + } + + public void setUsingDatabaseKeyword(boolean usingDatabaseKeyword) { + this.usingDatabaseKeyword = usingDatabaseKeyword; + } + + public AttachStatement withUsingDatabaseKeyword(boolean usingDatabaseKeyword) { + setUsingDatabaseKeyword(usingDatabaseKeyword); + return this; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + public AttachStatement withIfNotExists(boolean ifNotExists) { + setIfNotExists(ifNotExists); + return this; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public AttachStatement withOptions(List options) { + setOptions(options); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ATTACH "); + if (usingDatabaseKeyword) { + builder.append("DATABASE "); + } + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + builder.append(databasePath); + if (alias != null) { + builder.append(" AS ").append(alias); + } + if (options != null && !options.isEmpty()) { + builder.append(" ").append(PlainSelect.getStringList(options, true, true)); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/Block.java b/src/main/java/net/sf/jsqlparser/statement/Block.java index 83b9a23474..a4e822f01b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/Block.java +++ b/src/main/java/net/sf/jsqlparser/statement/Block.java @@ -9,7 +9,10 @@ */ package net.sf.jsqlparser.statement; -public class Block implements Statement { +import java.util.function.Consumer; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public class Block extends ASTNodeAccessImpl implements Statement { private boolean hasSemicolonAfterEnd = false; private Statements statements; @@ -36,9 +39,13 @@ public T accept(StatementVisitor statementVisitor, S context) { } public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer statementsPrinter) { builder.append("BEGIN\n"); if (statements != null) { - builder.append(statements); + statementsPrinter.accept(statements); } builder.append("END"); if (hasSemicolonAfterEnd) { diff --git a/src/main/java/net/sf/jsqlparser/statement/CascadeBehavior.java b/src/main/java/net/sf/jsqlparser/statement/CascadeBehavior.java new file mode 100644 index 0000000000..a67de27309 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/CascadeBehavior.java @@ -0,0 +1,18 @@ +/*- + * #%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.statement; + +/** + * An explicit CASCADE or RESTRICT clause. A null field leaves the clause omitted and preserves the + * database's default behavior for that statement. + */ +public enum CascadeBehavior { + CASCADE, RESTRICT +} diff --git a/src/main/java/net/sf/jsqlparser/statement/Commit.java b/src/main/java/net/sf/jsqlparser/statement/Commit.java index 7ce4650536..37205308fc 100644 --- a/src/main/java/net/sf/jsqlparser/statement/Commit.java +++ b/src/main/java/net/sf/jsqlparser/statement/Commit.java @@ -9,14 +9,57 @@ */ package net.sf.jsqlparser.statement; -public class Commit implements Statement { +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public class Commit extends ASTNodeAccessImpl implements Statement { + private TransactionKeyword keyword; + private TransactionChain chain; + + public TransactionKeyword getKeyword() { + return keyword; + } + + public void setKeyword(TransactionKeyword keyword) { + this.keyword = keyword; + } + + public Commit withKeyword(TransactionKeyword keyword) { + setKeyword(keyword); + return this; + } + + public TransactionChain getChain() { + return chain; + } + + public void setChain(TransactionChain chain) { + this.chain = chain; + } + + public Commit withChain(TransactionChain chain) { + setChain(chain); + return this; + } + + @Override public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); } + public StringBuilder appendTo(StringBuilder builder) { + builder.append("COMMIT"); + if (keyword != null) { + builder.append(' ').append(keyword); + } + if (chain != null) { + builder.append(' ').append(chain); + } + return builder; + } + @Override public String toString() { - return "COMMIT"; + return appendTo(new StringBuilder()).toString(); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/ConfigurationParameterChange.java b/src/main/java/net/sf/jsqlparser/statement/ConfigurationParameterChange.java new file mode 100644 index 0000000000..913cace5e6 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/ConfigurationParameterChange.java @@ -0,0 +1,175 @@ +/*- + * #%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.statement; + +import java.io.Serializable; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** + * A PostgreSQL {@code SET} or {@code RESET} of a configuration parameter, shared by statements that + * store parameter defaults: {@code ALTER ROLE}, {@code ALTER DATABASE} and the routine + * characteristics of {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} and {@code ALTER ROUTINE}: + * + *

+ * SET parameter { TO | = } { value [, ...] | DEFAULT }
+ * SET parameter FROM CURRENT
+ * RESET { parameter | ALL }
+ * 
+ * + * Parameter names keep their SQL spelling, including qualifiers and identifier quotes. + * {@code RESET ALL} uses the parameter name {@code ALL}. + */ +public class ConfigurationParameterChange implements Serializable { + + public enum Action { + SET, RESET + } + + /** What a {@code SET} assigns. Only the selected kind is rendered. */ + public enum ValueKind { + /** The explicit values of {@link #getValues()}. */ + VALUES, + /** {@code TO DEFAULT} or {@code = DEFAULT}. */ + DEFAULT, + /** {@code FROM CURRENT}. */ + FROM_CURRENT + } + + private Action action = Action.SET; + private String parameter; + private ValueKind valueKind = ValueKind.VALUES; + private boolean useEquals; + private ExpressionList values; + + public ConfigurationParameterChange() {} + + public ConfigurationParameterChange(Action action, String parameter) { + this.action = action; + this.parameter = parameter; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + public String getParameter() { + return parameter; + } + + public void setParameter(String parameter) { + this.parameter = parameter; + } + + public ValueKind getValueKind() { + return valueKind; + } + + /** + * Selects what a {@code SET} assigns. The values are retained when another kind is selected. + * + * @param valueKind the assigned value kind + */ + public void setValueKind(ValueKind valueKind) { + this.valueKind = valueKind; + } + + /** + * Whether {@code =} rather than {@code TO} precedes the value. + * + * @return {@code true} for {@code =} + */ + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + /** + * The values assigned by {@link ValueKind#VALUES}, as a mutable list. + * + * @return the values, or {@code null} if none were set + */ + public ExpressionList getValues() { + return values; + } + + public void setValues(ExpressionList values) { + this.values = values; + } + + public ConfigurationParameterChange withAction(Action action) { + setAction(action); + return this; + } + + public ConfigurationParameterChange withParameter(String parameter) { + setParameter(parameter); + return this; + } + + public ConfigurationParameterChange withValueKind(ValueKind valueKind) { + setValueKind(valueKind); + return this; + } + + public ConfigurationParameterChange withUseEquals(boolean useEquals) { + setUseEquals(useEquals); + return this; + } + + public ConfigurationParameterChange withValues(ExpressionList values) { + setValues(values); + return this; + } + + /** + * Appends this clause to a builder. + * + * @param builder the target + * @param valueRenderer appends each value, for example through an expression deparser + * @return the builder + */ + public StringBuilder appendTo(StringBuilder builder, Consumer valueRenderer) { + if (action == Action.RESET) { + return builder.append("RESET ").append(parameter); + } + builder.append("SET ").append(parameter); + if (valueKind == ValueKind.FROM_CURRENT) { + return builder.append(" FROM CURRENT"); + } + builder.append(useEquals ? " = " : " TO "); + if (valueKind == ValueKind.DEFAULT) { + return builder.append("DEFAULT"); + } + if (values != null) { + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + builder.append(", "); + } + valueRenderer.accept(values.get(i)); + } + } + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/ConnectStatement.java b/src/main/java/net/sf/jsqlparser/statement/ConnectStatement.java new file mode 100644 index 0000000000..829410b280 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/ConnectStatement.java @@ -0,0 +1,71 @@ +/*- + * #%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.statement; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * DuckDB's {@code CONNECT target [AS alias]}, which routes the session's queries to a remote + * database. + */ +public class ConnectStatement extends ASTNodeAccessImpl implements Statement { + private String target; + private String alias; + + public ConnectStatement() {} + + public ConnectStatement(String target) { + this.target = target; + } + + public String getTarget() { + return target; + } + + public void setTarget(String target) { + this.target = target; + } + + public ConnectStatement withTarget(String target) { + setTarget(target); + return this; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public ConnectStatement withAlias(String alias) { + setAlias(alias); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CONNECT ").append(target); + if (alias != null) { + builder.append(" AS ").append(alias); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java b/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java new file mode 100644 index 0000000000..2697677dbb --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java @@ -0,0 +1,167 @@ +/*- + * #%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.statement; + +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; + +import java.util.List; +import java.util.function.Consumer; + +/** + * {@code COPY table [(columns)] FROM path [(options)]} and + * {@code COPY {table | (query)} TO path [(options)]}, the bulk import and export statement in + * DuckDB and PostgreSQL. + * + * @see COPY + */ +public class CopyStatement extends ASTNodeAccessImpl implements Statement { + private Table table; + private ExpressionList columns; + private Select select; + private boolean from; + private String path; + private List options; + private boolean withKeyword; + + public Table getTable() { + return table; + } + + public void setTable(Table table) { + this.table = table; + } + + public CopyStatement withTable(Table table) { + setTable(table); + return this; + } + + public ExpressionList getColumns() { + return columns; + } + + public void setColumns(ExpressionList columns) { + this.columns = columns; + } + + public CopyStatement withColumns(ExpressionList columns) { + setColumns(columns); + return this; + } + + public Select getSelect() { + return select; + } + + public void setSelect(Select select) { + this.select = select; + } + + public CopyStatement withSelect(Select select) { + setSelect(select); + return this; + } + + /** {@code true} for COPY ... FROM (import), {@code false} for COPY ... TO (export). */ + public boolean isFrom() { + return from; + } + + public void setFrom(boolean from) { + this.from = from; + } + + public CopyStatement withFrom(boolean from) { + setFrom(from); + return this; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public CopyStatement withPath(String path) { + setPath(path); + return this; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public CopyStatement withOptions(List options) { + setOptions(options); + return this; + } + + /** Whether the option list is introduced by the optional WITH keyword. */ + public boolean isWithKeyword() { + return withKeyword; + } + + public void setWithKeyword(boolean withKeyword) { + this.withKeyword = withKeyword; + } + + public CopyStatement withWithKeyword(boolean withKeyword) { + setWithKeyword(withKeyword); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append); + } + + /** Renders the query through the caller's select writer. */ + public StringBuilder appendTo(StringBuilder builder, Consumer
tableRenderer) { + builder.append("SET IDENTITY_INSERT "); + tableRenderer.accept(table); + return builder.append(on ? " ON" : " OFF"); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/SetPasswordStatement.java b/src/main/java/net/sf/jsqlparser/statement/SetPasswordStatement.java new file mode 100644 index 0000000000..4fa23f2ae4 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/SetPasswordStatement.java @@ -0,0 +1,131 @@ +/*- + * #%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.statement; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.user.UserAccount; + +/** + * MySQL/MariaDB {@code SET PASSWORD [FOR user] {= password | TO RANDOM} + * [REPLACE 'current'] [RETAIN CURRENT PASSWORD]}. The password is an expression, usually a string + * literal; MariaDB also accepts {@code PASSWORD('...')}. + */ +public class SetPasswordStatement extends ASTNodeAccessImpl implements Statement { + + private UserAccount account; + private Expression password; + private boolean random; + private StringValue currentCredential; + private boolean retainCurrentPassword; + + /** The account after {@code FOR}, or null for the current user. */ + public UserAccount getAccount() { + return account; + } + + public void setAccount(UserAccount account) { + this.account = account; + } + + /** The password after {@code =}; null for {@code TO RANDOM}. */ + public Expression getPassword() { + return password; + } + + public void setPassword(Expression password) { + this.password = password; + } + + /** Whether the statement generates a password with {@code TO RANDOM}. */ + public boolean isRandom() { + return random; + } + + public void setRandom(boolean random) { + this.random = random; + } + + /** The current password of {@code REPLACE 'current'}, or null when omitted. */ + public StringValue getCurrentCredential() { + return currentCredential; + } + + public void setCurrentCredential(StringValue currentCredential) { + this.currentCredential = currentCredential; + } + + public boolean isRetainCurrentPassword() { + return retainCurrentPassword; + } + + public void setRetainCurrentPassword(boolean retainCurrentPassword) { + this.retainCurrentPassword = retainCurrentPassword; + } + + /** Shares statement rendering while letting deparsers render the password expression. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) { + builder.append("SET PASSWORD"); + if (account != null) { + builder.append(" FOR ").append(account); + } + if (random) { + builder.append(" TO RANDOM"); + } else { + builder.append(" = "); + expressionRenderer.accept(password); + } + if (currentCredential != null) { + builder.append(" REPLACE ").append(currentCredential); + } + if (retainCurrentPassword) { + builder.append(" RETAIN CURRENT PASSWORD"); + } + return builder; + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public SetPasswordStatement withAccount(UserAccount account) { + setAccount(account); + return this; + } + + public SetPasswordStatement withPassword(Expression password) { + setPassword(password); + return this; + } + + public SetPasswordStatement withRandom(boolean random) { + setRandom(random); + return this; + } + + public SetPasswordStatement withCurrentCredential(StringValue currentCredential) { + setCurrentCredential(currentCredential); + return this; + } + + public SetPasswordStatement withRetainCurrentPassword(boolean retainCurrentPassword) { + setRetainCurrentPassword(retainCurrentPassword); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/SetRoleStatement.java b/src/main/java/net/sf/jsqlparser/statement/SetRoleStatement.java new file mode 100644 index 0000000000..afc38b8bd1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/SetRoleStatement.java @@ -0,0 +1,53 @@ +/*- + * #%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.statement; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.user.RoleSelection; + +/** + * MySQL/MariaDB {@code SET ROLE {DEFAULT | NONE | ALL [EXCEPT role, ...] | role, ...}}. PostgreSQL + * {@code SET [SESSION | LOCAL] ROLE name} remains a {@link SetStatement}. + */ +public class SetRoleStatement extends ASTNodeAccessImpl implements Statement { + + private RoleSelection roles; + + public SetRoleStatement() { + // empty constructor + } + + public SetRoleStatement(RoleSelection roles) { + this.roles = roles; + } + + public RoleSelection getRoles() { + return roles; + } + + public void setRoles(RoleSelection roles) { + this.roles = roles; + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + @Override + public String toString() { + return "SET ROLE " + roles; + } + + public SetRoleStatement withRoles(RoleSelection roles) { + setRoles(roles); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/SetStatement.java b/src/main/java/net/sf/jsqlparser/statement/SetStatement.java index 16878f7739..ff20a553de 100644 --- a/src/main/java/net/sf/jsqlparser/statement/SetStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/SetStatement.java @@ -11,18 +11,100 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; -import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; -public final class SetStatement implements Statement { +public final class SetStatement extends ASTNodeAccessImpl implements Statement { private final List values = new ArrayList<>(); private String effectParameter; + private OnOffOptions onOffOptions; + + /** Assignment separator; NONE also supports SET options and PostgreSQL TIME ZONE. */ + public enum AssignmentOperator { + NONE(""), EQUALS("="), COLON_EQUALS(":="), TO("TO"); + + private final String token; + + AssignmentOperator(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + } + + /** SQL Server options that share the SET option [, option] ON | OFF syntax. */ + public enum OnOffOption { + QUOTED_IDENTIFIER, CONCAT_NULL_YIELDS_NULL, CURSOR_CLOSE_ON_COMMIT, ARITHABORT, ARITHIGNORE, FMTONLY, NOCOUNT, NOEXEC, NUMERIC_ROUNDABORT, PARSEONLY, ANSI_DEFAULTS, ANSI_NULL_DFLT_OFF, ANSI_NULL_DFLT_ON, ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, FORCEPLAN, SHOWPLAN_ALL, SHOWPLAN_TEXT, SHOWPLAN_XML, IMPLICIT_TRANSACTIONS, REMOTE_PROC_TRANSACTIONS, XACT_ABORT; + + public static OnOffOption fromName(String name) { + for (OnOffOption option : values()) { + if (option.name().equalsIgnoreCase(name)) { + return option; + } + } + return null; + } + } + + /** A group of options sharing one ON/OFF value; separate from assignment expressions. */ + public static final class OnOffOptions implements Serializable { + private final List options; + private boolean on; + + public OnOffOptions(Collection options, boolean on) { + this.options = new ArrayList<>(options); + if (this.options.isEmpty() || this.options.contains(null)) { + throw new IllegalArgumentException("At least one non-null SET option is required"); + } + this.on = on; + } + + public List getOptions() { + return options; + } + + public boolean isOn() { + return on; + } + + public void setOn(boolean on) { + this.on = on; + } + + private StringBuilder appendTo(StringBuilder builder) { + if (options.isEmpty() || options.contains(null)) { + throw new IllegalStateException("Invalid SQL Server SET option group"); + } + for (int i = 0; i < options.size(); i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(options.get(i)); + } + return builder.append(on ? " ON" : " OFF"); + } + } + + public OnOffOptions getOnOffOptions() { + return onOffOptions; + } + + /** Selects SQL Server option syntax, clearing any existing assignments and scope. */ + public void setOnOffOptions(OnOffOptions onOffOptions) { + Objects.requireNonNull(onOffOptions, "onOffOptions"); + clear(); + this.onOffOptions = onOffOptions; + } public SetStatement() { // empty constructor @@ -33,7 +115,12 @@ public SetStatement(Object name, ExpressionList value) { } public void add(Object name, ExpressionList value, boolean useEqual) { - values.add(new NameExpr(name, value, useEqual)); + add(name, value, useEqual ? AssignmentOperator.EQUALS : AssignmentOperator.NONE); + } + + public void add(Object name, ExpressionList value, AssignmentOperator operator) { + onOffOptions = null; + values.add(new NameExpr(name, value, operator)); } public void remove(int idx) { @@ -45,7 +132,7 @@ public int getCount() { } public boolean isUseEqual(int idx) { - return values.get(idx).useEqual; + return values.get(idx).isUseEqual(); } public boolean isUseEqual() { @@ -62,10 +149,27 @@ public SetStatement withUseEqual(int idx, boolean useEqual) { } public SetStatement setUseEqual(int idx, boolean useEqual) { - values.get(idx).useEqual = useEqual; + values.get(idx).setUseEqual(useEqual); + return this; + } + + public AssignmentOperator getAssignmentOperator(int idx) { + return values.get(idx).operator; + } + + public AssignmentOperator getAssignmentOperator() { + return getAssignmentOperator(0); + } + + public SetStatement setAssignmentOperator(int idx, AssignmentOperator operator) { + values.get(idx).operator = Objects.requireNonNull(operator, "operator"); return this; } + public SetStatement setAssignmentOperator(AssignmentOperator operator) { + return setAssignmentOperator(0, operator); + } + public SetStatement withUseEqual(boolean useEqual) { this.setUseEqual(useEqual); return this; @@ -103,28 +207,48 @@ public void setExpressions(int idx, ExpressionList expressions) { values.get(idx).expressions = expressions; } - private String toString(NameExpr ne) { - return ne.name + (ne.useEqual ? " = " : " ") - + PlainSelect.getStringList(ne.expressions, true, false); + /** Shares statement punctuation with deparsers while allowing expression visitors. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) { + builder.append("SET "); + if (onOffOptions != null) { + if (!values.isEmpty() || effectParameter != null) { + throw new IllegalStateException( + "SET options cannot be combined with assignments or scope"); + } + return onOffOptions.appendTo(builder); + } + if (effectParameter != null) { + builder.append(effectParameter).append(" "); + } + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + builder.append(", "); + } + appendAssignment(builder, values.get(i), expressionRenderer); + } + return builder; } - @Override - public String toString() { - StringBuilder b = new StringBuilder("SET "); - if (effectParameter != null) { - b.append(effectParameter).append(" "); - } - boolean addComma = false; - for (NameExpr ne : values) { - if (addComma) { - b.append(", "); - } else { - addComma = true; + private static void appendAssignment(StringBuilder builder, NameExpr value, + Consumer expressionRenderer) { + builder.append(value.name).append(" "); + if (value.operator != AssignmentOperator.NONE) { + builder.append(value.operator.getToken()).append(" "); + } + if (value.expressions != null) { + for (int i = 0; i < value.expressions.size(); i++) { + if (i > 0) { + builder.append(", "); + } + expressionRenderer.accept((Expression) value.expressions.get(i)); } - b.append(toString(ne)); } + } - return b.toString(); + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); } public List getKeyValuePairs() { @@ -132,6 +256,7 @@ public List getKeyValuePairs() { } public void addKeyValuePairs(Collection keyValuePairs) { + onOffOptions = null; values.addAll(keyValuePairs); } @@ -140,6 +265,7 @@ public void addKeyValuePairs(NameExpr... keyValuePairs) { } public void clear() { + onOffOptions = null; values.clear(); effectParameter = null; } @@ -165,12 +291,16 @@ public SetStatement withEffectParameter(String effectParameter) { static class NameExpr implements Serializable { Object name; ExpressionList expressions; - boolean useEqual; + AssignmentOperator operator; public NameExpr(Object name, ExpressionList expressions, boolean useEqual) { + this(name, expressions, useEqual ? AssignmentOperator.EQUALS : AssignmentOperator.NONE); + } + + public NameExpr(Object name, ExpressionList expressions, AssignmentOperator operator) { this.name = name; this.expressions = expressions; - this.useEqual = useEqual; + this.operator = Objects.requireNonNull(operator, "operator"); } public Object getName() { @@ -190,11 +320,11 @@ public void setExpressions(ExpressionList expressions) { } public boolean isUseEqual() { - return useEqual; + return operator == AssignmentOperator.EQUALS; } public void setUseEqual(boolean useEqual) { - this.useEqual = useEqual; + operator = useEqual ? AssignmentOperator.EQUALS : AssignmentOperator.NONE; } } } diff --git a/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java b/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java index 51f1ed26d9..5182afc659 100644 --- a/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java @@ -9,29 +9,149 @@ */ package net.sf.jsqlparser.statement; -public class ShowColumnsStatement implements Statement { +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; - private String tableName; +/** + * A MySQL {@code SHOW [EXTENDED] [FULL] {COLUMNS | FIELDS} {FROM | IN} table [{FROM | IN} db] + * [LIKE 'pattern' | WHERE expr]} statement. + * + * @see MySQL SHOW COLUMNS + */ +public class ShowColumnsStatement extends ASTNodeAccessImpl implements Statement { + + private boolean extended; + private boolean full; + private boolean fieldsKeyword; + private SelectionMode tableSelectionMode = SelectionMode.FROM; + private Table table; + private SelectionMode dbSelectionMode = SelectionMode.FROM; + private String dbName; + private Expression likeExpression; + private Expression whereCondition; public ShowColumnsStatement() { // empty constructor } public ShowColumnsStatement(String tableName) { - this.tableName = tableName; + setTableName(tableName); + } + + public ShowColumnsStatement(Table table) { + this.table = table; + } + + public boolean isExtended() { + return extended; + } + + public void setExtended(boolean extended) { + this.extended = extended; + } + + public boolean isFull() { + return full; + } + + public void setFull(boolean full) { + this.full = full; + } + + /** Whether the statement is spelled with {@code FIELDS} instead of {@code COLUMNS}. */ + public boolean isFieldsKeyword() { + return fieldsKeyword; + } + + public void setFieldsKeyword(boolean fieldsKeyword) { + this.fieldsKeyword = fieldsKeyword; + } + + /** The keyword before the table name, {@code FROM} unless {@code IN} was written. */ + public SelectionMode getTableSelectionMode() { + return tableSelectionMode; + } + + public void setTableSelectionMode(SelectionMode tableSelectionMode) { + this.tableSelectionMode = tableSelectionMode; + } + + public Table getTable() { + return table; } + public void setTable(Table table) { + this.table = table; + } + + /** The table name as written, including any qualifier; derived from {@link #getTable()}. */ public String getTableName() { - return tableName; + return table == null ? null : table.getFullyQualifiedName(); } public void setTableName(String tableName) { - this.tableName = tableName; + this.table = tableName == null ? null : new Table(tableName); + } + + /** The keyword before the database name, {@code FROM} unless {@code IN} was written. */ + public SelectionMode getDbSelectionMode() { + return dbSelectionMode; + } + + public void setDbSelectionMode(SelectionMode dbSelectionMode) { + this.dbSelectionMode = dbSelectionMode; + } + + /** The database of the trailing {@code FROM db} or {@code IN db} clause, or null. */ + public String getDbName() { + return dbName; + } + + public void setDbName(String dbName) { + this.dbName = dbName; + } + + public Expression getLikeExpression() { + return likeExpression; + } + + public void setLikeExpression(Expression likeExpression) { + this.likeExpression = likeExpression; + } + + public Expression getWhereCondition() { + return whereCondition; + } + + public void setWhereCondition(Expression whereCondition) { + this.whereCondition = whereCondition; } @Override public String toString() { - return "SHOW COLUMNS FROM " + tableName; + StringBuilder builder = new StringBuilder("SHOW"); + if (extended) { + builder.append(" EXTENDED"); + } + if (full) { + builder.append(" FULL"); + } + builder.append(fieldsKeyword ? " FIELDS " : " COLUMNS ") + .append(tableSelectionMode == null ? SelectionMode.FROM : tableSelectionMode) + .append(' ').append(table); + if (dbName != null) { + builder.append(' ') + .append(dbSelectionMode == null ? SelectionMode.FROM : dbSelectionMode) + .append(' ').append(dbName); + } + if (likeExpression != null) { + builder.append(" LIKE ").append(likeExpression); + } else if (whereCondition != null) { + builder.append(" WHERE ").append(whereCondition); + } + return builder.toString(); } @Override @@ -43,4 +163,49 @@ public ShowColumnsStatement withTableName(String tableName) { this.setTableName(tableName); return this; } + + public ShowColumnsStatement withTable(Table table) { + this.setTable(table); + return this; + } + + public ShowColumnsStatement withExtended(boolean extended) { + this.setExtended(extended); + return this; + } + + public ShowColumnsStatement withFull(boolean full) { + this.setFull(full); + return this; + } + + public ShowColumnsStatement withFieldsKeyword(boolean fieldsKeyword) { + this.setFieldsKeyword(fieldsKeyword); + return this; + } + + public ShowColumnsStatement withTableSelectionMode(SelectionMode tableSelectionMode) { + this.setTableSelectionMode(tableSelectionMode); + return this; + } + + public ShowColumnsStatement withDbSelectionMode(SelectionMode dbSelectionMode) { + this.setDbSelectionMode(dbSelectionMode); + return this; + } + + public ShowColumnsStatement withDbName(String dbName) { + this.setDbName(dbName); + return this; + } + + public ShowColumnsStatement withLikeExpression(Expression likeExpression) { + this.setLikeExpression(likeExpression); + return this; + } + + public ShowColumnsStatement withWhereCondition(Expression whereCondition) { + this.setWhereCondition(whereCondition); + return this; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/ShowStatement.java b/src/main/java/net/sf/jsqlparser/statement/ShowStatement.java index c5f1729cc4..c90bdea44e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/ShowStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/ShowStatement.java @@ -9,7 +9,9 @@ */ package net.sf.jsqlparser.statement; -public class ShowStatement implements Statement { +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public class ShowStatement extends ASTNodeAccessImpl implements Statement { private String name; diff --git a/src/main/java/net/sf/jsqlparser/statement/StartTransaction.java b/src/main/java/net/sf/jsqlparser/statement/StartTransaction.java new file mode 100644 index 0000000000..b502be481b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/StartTransaction.java @@ -0,0 +1,106 @@ +/*- + * #%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.statement; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** START TRANSACTION, or BEGIN when a transaction dialect is selected. */ +public class StartTransaction extends ASTNodeAccessImpl implements Statement { + public enum Command { + START_TRANSACTION, BEGIN + } + + public enum Mode { + ISOLATION_LEVEL_SERIALIZABLE( + "ISOLATION LEVEL SERIALIZABLE"), ISOLATION_LEVEL_REPEATABLE_READ( + "ISOLATION LEVEL REPEATABLE READ"), ISOLATION_LEVEL_READ_COMMITTED( + "ISOLATION LEVEL READ COMMITTED"), ISOLATION_LEVEL_READ_UNCOMMITTED( + "ISOLATION LEVEL READ UNCOMMITTED"), READ_WRITE( + "READ WRITE"), READ_ONLY("READ ONLY"), DEFERRABLE( + "DEFERRABLE"), NOT_DEFERRABLE( + "NOT DEFERRABLE"), WITH_CONSISTENT_SNAPSHOT( + "WITH CONSISTENT SNAPSHOT"); + + private final String sql; + + Mode(String sql) { + this.sql = sql; + } + + @Override + public String toString() { + return sql; + } + } + + private Command command = Command.START_TRANSACTION; + private TransactionKeyword keyword; + private final List modes = new ArrayList<>(); + + public Command getCommand() { + return command; + } + + public void setCommand(Command command) { + this.command = Objects.requireNonNull(command, "command"); + } + + public StartTransaction withCommand(Command command) { + setCommand(command); + return this; + } + + public TransactionKeyword getKeyword() { + return keyword; + } + + public void setKeyword(TransactionKeyword keyword) { + this.keyword = keyword; + } + + public StartTransaction withKeyword(TransactionKeyword keyword) { + setKeyword(keyword); + return this; + } + + /** Mutable, ordered transaction characteristics. */ + public List getModes() { + return modes; + } + + public StartTransaction addMode(Mode mode) { + modes.add(Objects.requireNonNull(mode, "mode")); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(command == Command.BEGIN ? "BEGIN" : "START TRANSACTION"); + if (command == Command.BEGIN && keyword != null) { + builder.append(' ').append(keyword); + } + for (int i = 0; i < modes.size(); i++) { + builder.append(i == 0 ? " " : ", ").append(modes.get(i)); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/Statement.java b/src/main/java/net/sf/jsqlparser/statement/Statement.java index 380092ab68..57e6e67def 100644 --- a/src/main/java/net/sf/jsqlparser/statement/Statement.java +++ b/src/main/java/net/sf/jsqlparser/statement/Statement.java @@ -11,10 +11,26 @@ import net.sf.jsqlparser.Model; +import java.util.function.Predicate; + public interface Statement extends Model { T accept(StatementVisitor statementVisitor, S context); default void accept(StatementVisitor statementVisitor) { accept(statementVisitor, null); } + + /** + * Static feature analysis of this statement tree. Not cached: the AST is mutable and publicly + * constructible, and a stale bitmask is worse than a recomputation (micro seconds against a + * millisecond-scale parse). + */ + default StatementFeatures getFeatures() { + return StatementFeatureVisitor.analyse(this); + } + + /** Same, with a caller-supplied allow-list of provably side-effect-free functions. */ + default StatementFeatures getFeatures(Predicate pureFunctions) { + return StatementFeatureVisitor.analyse(this, pureFunctions); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java new file mode 100644 index 0000000000..8f4e7aa8f7 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -0,0 +1,1385 @@ +/*- + * #%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.statement; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Predicate; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.AnalyticExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.TranscodingFunction; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterRoutine; +import net.sf.jsqlparser.statement.alter.AlterTrigger; +import net.sf.jsqlparser.statement.alter.AlterDomain; +import net.sf.jsqlparser.statement.alter.AlterExtension; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.alter.AlterPublication; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.alter.AlterSession; +import net.sf.jsqlparser.statement.alter.AlterStatistics; +import net.sf.jsqlparser.statement.alter.AlterSubscription; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; +import net.sf.jsqlparser.statement.alter.AlterSystemStatement; +import net.sf.jsqlparser.statement.alter.AlterTablespace; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterType; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; +import net.sf.jsqlparser.statement.alter.RenameTableStatement; +import net.sf.jsqlparser.statement.alter.AlterUser; +import net.sf.jsqlparser.statement.alter.RenameUser; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +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.accessmethod.CreateAccessMethod; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.create.database.CreateDatabase; +import net.sf.jsqlparser.statement.create.domain.CreateDomain; +import net.sf.jsqlparser.statement.create.extension.CreateExtension; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.policy.CreatePolicy; +import net.sf.jsqlparser.statement.create.publication.CreatePublication; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.create.schema.CreateSchema; +import net.sf.jsqlparser.statement.create.sequence.CreateSequence; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; +import net.sf.jsqlparser.statement.create.subscription.CreateSubscription; +import net.sf.jsqlparser.statement.create.subscription.SubscriptionOption; +import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; +import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.type.CreateType; +import net.sf.jsqlparser.statement.create.user.CreateUser; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.create.view.AlterView; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; +import net.sf.jsqlparser.statement.execute.Execute; +import net.sf.jsqlparser.statement.export.Export; +import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; +import net.sf.jsqlparser.statement.grant.Grant; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.imprt.Import; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.insert.InsertBulk; +import net.sf.jsqlparser.statement.load.LoadDataStatement; +import net.sf.jsqlparser.statement.lock.LockStatement; +import net.sf.jsqlparser.statement.merge.Merge; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; +import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.ReassignOwned; +import net.sf.jsqlparser.statement.select.FromItemVisitor; +import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause; +import net.sf.jsqlparser.statement.select.PivotVisitor; +import net.sf.jsqlparser.statement.select.PivotVisitorAdapter; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.statement.select.SelectItemVisitor; +import net.sf.jsqlparser.statement.select.SelectItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.statement.select.TableStatement; +import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; +import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; +import net.sf.jsqlparser.statement.show.ShowTablesStatement; +import net.sf.jsqlparser.statement.truncate.Truncate; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.statement.upsert.Upsert; + +/** + * Derives a {@link StatementFeatures} verdict from a statement tree. + * + *

+ * Built on the existing adapter chain rather than on a hand-written traversal: only the nodes that + * contribute a feature are overridden, everything else is inherited from + * {@link StatementVisitorAdapter} / {@link SelectVisitorAdapter} / {@link ExpressionVisitorAdapter} + * / {@link FromItemVisitorAdapter}. + * + *

Why a top-level flag and not a flag union

+ * + * {@code RETURNS_RESULT_SET} is a property of the statement's own result position, not of whatever + * happens to appear somewhere in the tree: + * + *
+ *   SELECT * FROM t                                        RETURNS_RESULT_SET, READS_DATA
+ *   INSERT INTO x SELECT * FROM t                          READS_DATA, MODIFIES_DATA  (no result set)
+ *   DELETE FROM t RETURNING *                              RETURNS_RESULT_SET, MODIFIES_DATA
+ *   WITH c AS (DELETE FROM t RETURNING *) SELECT * FROM c  RETURNS_RESULT_SET, MODIFIES_DATA
+ *   INSERT INTO x WITH c AS (DELETE .. RETURNING *) SELECT MODIFIES_DATA            (no result set)
+ * 
+ * + * The first statement visit claims the top level; every nested statement - reached through a + * data-modifying CTE, a subquery or an IF block - finds it already claimed and can therefore + * contribute {@code MODIFIES_DATA} but never {@code RETURNS_RESULT_SET}. + * + *

Two upstream defects this class works around

+ * + *
    + *
  1. {@code SelectVisitorAdapter#visit(WithItem)} calls {@code withItem.getSelect()}, an unchecked + * cast to {@code ParenthesedSelect}. A data-modifying CTE reached through the select path + * - i.e. from a subquery, via {@code ExpressionVisitorAdapter#visit(Select)} - throws + * ClassCastException. {@link FeatureSelectVisitor} routes through {@code getParenthesedStatement()} + * instead. Worth fixing in the adapter itself.
  2. + *
  3. {@code StatementVisitorAdapter#visit(Delete)} never calls {@code visitReturningClause}, + * unlike its Insert and Update counterparts, so expressions in {@code DELETE .. RETURNING f(x)} are + * not traversed. Walked explicitly here.
  4. + *
+ */ +@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.TooManyMethods", "PMD.ExcessiveImports"}) +public class StatementFeatureVisitor extends StatementVisitorAdapter { + + private final Analysis analysis; + + // ---- construction ----------------------------------------------------------------------- + + private StatementFeatureVisitor(Analysis analysis) { + super(wire(analysis)); + this.analysis = analysis; + analysis.statements = this; + } + + /** + * Builds the mutually recursive visitor chain. The cycle (expression -> select -> from + * item -> expression) is closed after construction through the adapters' setters, because + * neither adapter can be handed a reference to something not yet constructed. + */ + private static SelectVisitorAdapter wire(Analysis analysis) { + FeatureExpressionVisitor expressions = new FeatureExpressionVisitor(analysis); + FromItemVisitorAdapter fromItems = new FromItemVisitorAdapter<>(); + + FeatureSelectVisitor selects = new FeatureSelectVisitor(analysis, expressions, + new PivotVisitorAdapter<>(expressions), + new SelectItemVisitorAdapter<>(expressions), fromItems); + + expressions.setSelectVisitor(selects); + fromItems.setSelectVisitor((SelectVisitor) selects).setExpressionVisitor(expressions); + + analysis.expressions = expressions; + analysis.selects = selects; + return selects; + } + + public static StatementFeatures analyse(Statement statement) { + return analyse(statement, name -> false); + } + + /** + * @param pureFunctions names (lower case) the caller can prove side-effect free, e.g. from + * {@code pg_proc.provolatile} or a static allow-list. Anything not proven pure leaves + * {@code MODIFIES_DATA} in the possible set. + */ + public static StatementFeatures analyse(Statement statement, Predicate pureFunctions) { + Analysis analysis = new Analysis(pureFunctions); + StatementFeatureVisitor visitor = new StatementFeatureVisitor(analysis); + statement.accept(visitor, null); + analysis.failLoudIfSilent(); + return new StatementFeatures(analysis.certain, analysis.uncertain, analysis.unresolved); + } + + public static StatementFeatures analyse(String sql) throws JSQLParserException { + return analyse(CCJSqlParserUtil.parse(sql)); + } + + /** + * Aggregate verdict over a whole script. + * + *

+ * {@link Statements} is an {@code ArrayList}, not a {@link Statement}, so it cannot + * go through {@link #analyse(Statement)} and has no {@code getFeatures()} default method - + * hence this overload. + * + *

+ * Read the result as a union, and only for guard purposes. Each statement in the script + * claims its own result position, so {@code UPDATE ..; SELECT ..;} yields both + * {@code MODIFIES_DATA} and {@code RETURNS_RESULT_SET} - which is the right answer for "may + * this script write?" and a meaningless one for "executeQuery or executeUpdate?", since the + * union says nothing about which statement returns rows. Dispatchers want + * {@link #analyseEach(Statements)}. + */ + public static StatementFeatures analyse(Statements statements) { + return analyse(statements, name -> false); + } + + public static StatementFeatures analyse(Statements statements, + Predicate pureFunctions) { + Analysis analysis = new Analysis(pureFunctions); + StatementFeatureVisitor visitor = new StatementFeatureVisitor(analysis); + statements.accept(visitor, null); + analysis.failLoudIfSilent(); + return new StatementFeatures(analysis.certain, analysis.uncertain, analysis.unresolved); + } + + /** + * One verdict per statement, in script order. This is what a dispatcher needs: it answers "does + * statement n return rows?" instead of "does anything in here return rows?". Each + * statement is analysed independently, so no feature leaks between them. + */ + public static List analyseEach(Statements statements) { + return analyseEach(statements, name -> false); + } + + public static List analyseEach(Statements statements, + Predicate pureFunctions) { + List perStatement = new ArrayList<>(statements.size()); + for (Statement statement : statements) { + perStatement.add(analyse(statement, pureFunctions)); + } + return perStatement; + } + + // ---- shared state ----------------------------------------------------------------------- + + /** + * The {@code S context} argument cannot carry this state: + * {@code StatementVisitorAdapter#visit(Insert)} passes a hardcoded {@code null} context down to + * the select visitor, so anything threaded through the context is lost at exactly the node that + * matters most. + */ + static final class Analysis { + final EnumSet certain = EnumSet.noneOf(StmtFeature.class); + final EnumSet uncertain = EnumSet.noneOf(StmtFeature.class); + final Set unresolved = new LinkedHashSet<>(); + final Predicate pureFunctions; + + StatementVisitor statements; + ExpressionVisitor expressions; + SelectVisitor selects; + + private boolean topLevelClaimed; + private int readsSuppressed; + + Analysis(Predicate pureFunctions) { + this.pureFunctions = pureFunctions; + } + + /** True exactly once per top-level statement; false for everything nested. */ + boolean claimTopLevel() { + if (topLevelClaimed) { + return false; + } + topLevelClaimed = true; + return true; + } + + void releaseTopLevel() { + topLevelClaimed = false; + } + + void certain(StmtFeature... features) { + for (StmtFeature feature : features) { + if (feature == StmtFeature.READS_DATA && readsSuppressed > 0) { + continue; + } + certain.add(feature); + } + } + + void possible(StmtFeature... features) { + for (StmtFeature feature : features) { + uncertain.add(feature); + } + } + + void unresolved(String reference) { + unresolved.add(reference.toLowerCase(Locale.ROOT)); + } + + /** A view definition references tables without reading them. */ + void suppressReads(Runnable body) { + readsSuppressed++; + try { + body.run(); + } finally { + readsSuppressed--; + } + } + + void opaque(String reference) { + opaque(); + unresolved(reference); + } + + void opaque() { + certain.add(StmtFeature.OPAQUE); + uncertain.addAll(EnumSet.of(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET, + StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA)); + } + + /** + * A statement that contributed nothing was not classified - almost certainly a statement + * type added to the grammar after this visitor was written, whose inherited adapter method + * silently returns null. Degrade loudly, never to "harmless". This is what replaces a + * hand-maintained type table; the price is that a genuinely inert statement is also + * reported opaque, which is the right direction to be wrong in. + */ + void failLoudIfSilent() { + if (certain.isEmpty()) { + opaque(); + } + } + } + + // ---- result-producing clauses ----------------------------------------------------------- + + /** + * RETURNING always ships rows to the client. So does OUTPUT - unless the rows are redirected, + * which the grammar allows in two ways: + * + *

+     *   OUTPUT DELETED.*                    -> rows go to the client
+     *   OUTPUT DELETED.id INTO audit        -> rows go to a table       (getOutputTable())
+     *   OUTPUT DELETED.id INTO @MyTableVar  -> rows go to a table variable (getTableVariable())
+     * 
+ * + * Both redirections have to be checked. {@code OutputClause()} in the grammar is + * {@code ( UserVariable() | Table() )}, so the table-variable form leaves + * {@code getOutputTable()} null - testing only that one would report + * {@code OUTPUT .. INTO @MyTableVar} as returning a result set, which it does not. + */ + private void resultClauses(boolean topLevel, ReturningClause returning, OutputClause output) { + if (!topLevel) { + return; + } + if (returning != null && !returning.isEmpty()) { + analysis.certain(StmtFeature.RETURNS_RESULT_SET); + } + if (output != null && output.getSelectItemList() != null + && output.getOutputTable() == null && output.getTableVariable() == null) { + analysis.certain(StmtFeature.RETURNS_RESULT_SET); + } + if (output != null && (output.getOutputTable() != null + || output.getTableVariable() != null)) { + // OUTPUT .. INTO
writes rows somewhere else + analysis.certain(StmtFeature.MODIFIES_DATA); + } + } + + /** Works around defect (2): the adapter does not walk DELETE's RETURNING expressions. */ + private void walkReturning(ReturningClause returning) { + if (returning == null) { + return; + } + for (SelectItem item : returning) { + Expression expression = item.getExpression(); + if (expression != null) { + expression.accept(analysis.expressions, null); + } + } + } + + // ---- DML -------------------------------------------------------------------------------- + + @Override + public Void visit(Select select, S context) { + if (analysis.claimTopLevel()) { + analysis.certain(StmtFeature.RETURNS_RESULT_SET); + } + // READS_DATA is decided in FeatureSelectVisitor - "SELECT 1" reads nothing. + return super.visit(select, context); + } + + @Override + public Void visit(Insert insert, S context) { + boolean topLevel = analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_DATA); + resultClauses(topLevel, insert.getReturningClause(), insert.getOutputClause()); + if (insert.getSelect() != null) { + analysis.certain(StmtFeature.READS_DATA); + } + return super.visit(insert, context); + } + + @Override + public Void visit(InsertBulk statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_DATA); + return super.visit(statement, context); + } + + @Override + public Void visit(Update update, S context) { + boolean topLevel = analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_DATA); + resultClauses(topLevel, update.getReturningClause(), update.getOutputClause()); + return super.visit(update, context); + } + + @Override + public Void visit(Delete delete, S context) { + boolean topLevel = analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_DATA); + resultClauses(topLevel, delete.getReturningClause(), delete.getOutputClause()); + super.visit(delete, context); + walkReturning(delete.getReturningClause()); + return null; + } + + @Override + public Void visit(Merge merge, S context) { + boolean topLevel = analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_DATA, StmtFeature.READS_DATA); + resultClauses(topLevel, null, merge.getOutputClause()); + return super.visit(merge, context); + } + + @Override + public Void visit(Upsert upsert, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_DATA); + if (upsert.getSelect() != null) { + analysis.certain(StmtFeature.READS_DATA); + upsert.getSelect().accept(analysis.selects, context); + } + return null; + } + + @Override + public Void visit(Import imprt, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_DATA); + return null; + } + + @Override + public Void visit(Export export, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.READS_DATA); + if (export.getSelect() != null) { + export.getSelect().accept(analysis.selects, context); + } + return null; + } + + // ---- DDL -------------------------------------------------------------------------------- + + @Override + public Void visit(CreateTable createTable, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + if (createTable.getSelect() != null) { + // CTAS reads; it does not return a result set - the top level is already claimed + analysis.certain(StmtFeature.READS_DATA); + } + return super.visit(createTable, context); + } + + @Override + public Void visit(CreateView createView, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + if (createView.isMaterialized() && !Boolean.FALSE.equals(createView.getWithData())) { + if (createView.getSelect() != null) { + createView.getSelect().accept(analysis.selects, context); + } + } else { + analysis.suppressReads(() -> { + if (createView.getSelect() != null) { + createView.getSelect().accept(analysis.selects, null); + } + }); + } + return null; + } + + @Override + public Void visit(AlterView alterView, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + analysis.suppressReads(() -> { + if (alterView.getSelect() != null) { + alterView.getSelect().accept(analysis.selects, null); + } + }); + return null; + } + + @Override + public Void visit(RefreshMaterializedViewStatement materializedView, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA, StmtFeature.MODIFIES_DATA, + StmtFeature.READS_DATA); + return null; + } + + @Override + public Void visit(Truncate truncate, S context) { + analysis.claimTopLevel(); + // DDL in most engines (implicit commit, no row triggers) but it destroys rows: + // a read-only guard must see MODIFIES_DATA here + analysis.certain(StmtFeature.MODIFIES_SCHEMA, StmtFeature.MODIFIES_DATA); + return null; + } + + @Override + public Void visit(Drop drop, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA, StmtFeature.MODIFIES_DATA); + return null; + } + + @Override + public Void visit(ReassignOwned statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(PragmaStatement pragmaStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(AssertStatement assertStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.READS_DATA); + return null; + } + + @Override + public Void visit(ExtensionStatement extensionStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(AttachStatement attachStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(DetachStatement detachStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(ConnectStatement connectStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(DisconnectStatement disconnectStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(PrepareStatement prepareStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(DeallocateStatement deallocateStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(CopyStatement copyStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.READS_DATA, StmtFeature.MODIFIES_DATA); + return null; + } + + @Override + public Void visit(ExportDataStatement exportDataStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.READS_DATA, StmtFeature.MODIFIES_DATA); + return null; + } + + @Override + public Void visit(CreateMacro createMacro, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + @Override + public Void visit(CreateExtensionRepository createExtensionRepository, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + @Override + public Void visit(LoadDataStatement loadDataStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_DATA); + return null; + } + + @Override + public Void visit(PurgeStatement purgeStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA, StmtFeature.MODIFIES_DATA); + return null; + } + + @Override + public Void visit(Alter alter, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateIndex createIndex, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateSchema createSchema, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateDatabase createDatabase, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateSequence createSequence, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterSequence alterSequence, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateSynonym createSynonym, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreatePolicy createPolicy, S context) { + return schemaOnly(); + } + + @Override + public Void visit(RenameTableStatement renameTableStatement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(Comment comment, S context) { + return schemaOnly(); + } + + @Override + public Void visit(Grant grant, S context) { + return schemaOnly(); + } + + @Override + public Void visit(Analyze analyze, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA, StmtFeature.READS_DATA); // rewrites + // statistics + return null; + } + + @Override + public Void visit(TableMaintenanceStatement statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET); + if (statement.getOperation() == TableMaintenanceStatement.Operation.OPTIMIZE + || statement.getOperation() == TableMaintenanceStatement.Operation.REPAIR) { + // OPTIMIZE rebuilds table storage; REPAIR may also rewrite or discard rows + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + if (statement.getOperation() == TableMaintenanceStatement.Operation.REPAIR) { + analysis.possible(StmtFeature.MODIFIES_DATA); + } + } + return null; + } + + private Void schemaOnly() { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + // ---- opaque ----------------------------------------------------------------------------- + + @Override + public Void visit(Execute execute, S context) { + analysis.claimTopLevel(); + analysis.opaque(String.valueOf(execute.getName())); + return null; + } + + @Override + public Void visit(DoStatement statement, S context) { + analysis.claimTopLevel(); + analysis.opaque("DO"); + return null; + } + + @Override + public Void visit(CreateFunctionalStatement createFunctionalStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + // the body is an opaque token list - nothing about it is knowable here + analysis.possible(StmtFeature.MODIFIES_DATA, StmtFeature.READS_DATA); + analysis.unresolved(createFunctionalStatement.getClass().getSimpleName()); + return null; + } + + @Override + public Void visit(UnsupportedStatement unsupportedStatement, S context) { + analysis.claimTopLevel(); + analysis.opaque("unsupported"); + return null; + } + + @Override + public Void visit(ExplainStatement explainStatement, S context) { + if (analysis.claimTopLevel()) { + analysis.certain(StmtFeature.RETURNS_RESULT_SET); + } + // EXPLAIN ANALYZE executes the statement. The option set is dialect-specific, so the + // side effects of the explained statement stay in the possible set. + analysis.possible(StmtFeature.MODIFIES_DATA, StmtFeature.READS_DATA); + analysis.unresolved("explain"); + if (explainStatement.getStatement() != null) { + explainStatement.getStatement().accept(this, context); + } + return null; + } + + // ---- control flow ----------------------------------------------------------------------- + + @Override + public Void visit(Statements statements, S context) { + for (Statement statement : statements) { + analysis.releaseTopLevel(); // each statement in a script has its own result position + statement.accept(this, context); + } + analysis.releaseTopLevel(); + return null; + } + + @Override + public Void visit(Block block, S context) { + analysis.claimTopLevel(); + // Statements is itself a List; getStatements() is deprecated + Statements inner = block.getStatements(); + if (inner != null) { + for (Statement statement : inner) { + statement.accept(this, context); + } + } + return null; + } + + @Override + public Void visit(IfElseStatement ifElseStatement, S context) { + analysis.claimTopLevel(); + // both branches contribute: this is what the statement *may* do, not what it will do + return super.visit(ifElseStatement, context); + } + + // ---- session / transaction -------------------------------------------------------------- + + @Override + public Void visit(StartTransaction statement, S context) { + return transactionOnly(); + } + + @Override + public Void visit(ReleaseSavepointStatement statement, S context) { + return transactionOnly(); + } + + @Override + public Void visit(Commit commit, S context) { + return transactionOnly(); + } + + @Override + public Void visit(RollbackStatement rollbackStatement, S context) { + return transactionOnly(); + } + + @Override + public Void visit(SavepointStatement savepointStatement, S context) { + return transactionOnly(); + } + + @Override + public Void visit(LockStatement lock, S context) { + return transactionOnly(); + } + + private Void transactionOnly() { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_TRANSACTION); + return null; + } + + @Override + public Void visit(SetStatement set, S context) { + return sessionOnly(); + } + + @Override + public Void visit(ResetStatement reset, S context) { + return sessionOnly(); + } + + @Override + public Void visit(SetIdentityInsertStatement statement, S context) { + return sessionOnly(); + } + + @Override + public Void visit(SetRoleStatement statement, S context) { + return sessionOnly(); + } + + @Override + public Void visit(UseStatement use, S context) { + return sessionOnly(); + } + + @Override + public Void visit(SessionStatement sessionStatement, S context) { + return sessionOnly(); + } + + @Override + public Void visit(AlterSession alterSession, S context) { + return sessionOnly(); + } + + @Override + public Void visit(DeclareStatement declareStatement, S context) { + return sessionOnly(); + } + + @Override + public Void visit(AlterSystemStatement alterSystemStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + analysis.possible(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + private Void sessionOnly() { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + // ---- metadata readers ------------------------------------------------------------------- + + @Override + public Void visit(ShowStatement showStatement, S context) { + return metadataOnly(); + } + + @Override + public Void visit(ShowColumnsStatement showColumnsStatement, S context) { + return metadataOnly(); + } + + @Override + public Void visit(ShowIndexStatement showIndexStatement, S context) { + return metadataOnly(); + } + + @Override + public Void visit(ShowTablesStatement showTables, S context) { + return metadataOnly(); + } + + @Override + public Void visit(ShowTableStatusStatement showTableStatus, S context) { + return metadataOnly(); + } + + @Override + public Void visit(ShowCreateStatement showCreate, S context) { + return metadataOnly(); + } + + @Override + public Void visit(DescribeStatement describe, S context) { + return metadataOnly(); + } + + /** Reads the catalogue, not the data. */ + private Void metadataOnly() { + if (analysis.claimTopLevel()) { + analysis.certain(StmtFeature.RETURNS_RESULT_SET); + } else { + analysis.certain(StmtFeature.OPAQUE); // keeps failLoudIfSilent honest when nested + } + return null; + } + + // ---- nested visitors -------------------------------------------------------------------- + + static final class FeatureSelectVisitor extends SelectVisitorAdapter { + private final Analysis analysis; + + FeatureSelectVisitor(Analysis analysis, ExpressionVisitor expressionVisitor, + PivotVisitor pivotVisitor, SelectItemVisitor selectItemVisitor, + FromItemVisitor fromItemVisitor) { + super(expressionVisitor, pivotVisitor, selectItemVisitor, fromItemVisitor); + this.analysis = analysis; + } + + @Override + public Void visit(PlainSelect plainSelect, S context) { + if (plainSelect.getFromItem() != null + || plainSelect.getJoins() != null && !plainSelect.getJoins().isEmpty()) { + analysis.certain(StmtFeature.READS_DATA); + } + + // SELECT .. INTO
materialises rows instead of returning them, so the + // RETURNS_RESULT_SET claimed by the statement visit has to be withdrawn + boolean into = plainSelect.getIntoTables() != null + && !plainSelect.getIntoTables().isEmpty() + || plainSelect.getIntoTempTable() != null; + if (into) { + analysis.certain(StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA); + analysis.certain.remove(StmtFeature.RETURNS_RESULT_SET); + } + + MySqlSelectIntoClause mySqlInto = plainSelect.getMySqlSelectIntoClause(); + if (mySqlInto != null && mySqlInto.getType() == MySqlSelectIntoClause.Type.VARIABLES) { + analysis.certain(StmtFeature.MODIFIES_SESSION); + analysis.certain.remove(StmtFeature.RETURNS_RESULT_SET); + } + + if (plainSelect.getForMode() != null) { + // FOR UPDATE / FOR SHARE take row locks + analysis.certain(StmtFeature.MODIFIES_TRANSACTION); + } + + return super.visit(plainSelect, context); + } + + @Override + public Void visit(TableStatement tableStatement, S context) { + analysis.certain(StmtFeature.READS_DATA); + return super.visit(tableStatement, context); + } + + /** + * Replaces the inherited implementation, which casts the CTE body to ParenthesedSelect and + * therefore throws on {@code WITH c AS (DELETE .. RETURNING *)}. Routing to the statement + * visitor is also what makes a data-modifying CTE contribute MODIFIES_DATA. + */ + @Override + public Void visit(WithItem withItem, S context) { + ParenthesedStatement body = withItem.getParenthesedStatement(); + if (body instanceof Statement) { + ((Statement) body).accept(analysis.statements, context); + } + if (withItem.getExpression() != null) { + withItem.getExpression().accept(new FeatureExpressionVisitor(analysis), context); + } + return null; + } + } + + static final class FeatureExpressionVisitor extends ExpressionVisitorAdapter { + private final Analysis analysis; + + FeatureExpressionVisitor(Analysis analysis) { + this.analysis = analysis; + } + + /** Records unproven functions consistently across their different expression models. */ + private void analyseFunction(String functionName) { + String name = functionName == null ? "?" : functionName.toLowerCase(Locale.ROOT); + if (!analysis.pureFunctions.test(name)) { + analysis.possible(StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA); + analysis.unresolved(name); + } + } + + @Override + public Void visit(Function function, S context) { + analyseFunction(function.getName()); + return super.visit(function, context); + } + + @Override + public Void visit(AnalyticExpression expression, S context) { + analyseFunction(expression.getName()); + return super.visit(expression, context); + } + + @Override + public Void visit(TranscodingFunction expression, S context) { + analyseFunction(expression.getKeyword()); + return super.visit(expression, context); + } + } + + @Override + public Void visit(CreateRole statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterRole statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(Revoke statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterDefaultPrivileges statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateTrigger statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateEventTrigger statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterEventTrigger statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(Reindex statement, S context) { + analysis.claimTopLevel(); + // rebuilds index storage from the indexed rows + analysis.certain(StmtFeature.MODIFIES_SCHEMA, StmtFeature.READS_DATA); + return null; + } + + @Override + public Void visit(SecurityLabel statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateType statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + @Override + public Void visit(AlterType statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + @Override + public Void visit(CreateDomain statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + @Override + public Void visit(AlterDomain statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + @Override + public Void visit(CreateExtension statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + @Override + public Void visit(AlterExtension statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + @Override + public Void visit(CreatePublication statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + + return null; + } + + @Override + public Void visit(AlterPublication statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + + return null; + } + + @Override + public Void visit(CreateSubscription statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + if (statement.getOptions().stream() + .noneMatch(option -> (option.getKind() == SubscriptionOption.Kind.CONNECT + || option.getKind() == SubscriptionOption.Kind.ENABLED) + && Boolean.FALSE.equals(option.getBooleanValue()))) { + // A subscription may start asynchronous replication; the remote contents are unknown. + analysis.possible(StmtFeature.MODIFIES_DATA); + } + return null; + } + + @Override + public Void visit(AlterSubscription statement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + if (statement.getAction() == AlterSubscription.Action.ENABLE + || statement.getAction() == AlterSubscription.Action.REFRESH_PUBLICATION + || statement.getAction() == AlterSubscription.Action.SET_PUBLICATION + || statement.getAction() == AlterSubscription.Action.ADD_PUBLICATION + || statement.getAction() == AlterSubscription.Action.DROP_PUBLICATION) { + analysis.possible(StmtFeature.MODIFIES_DATA); + } + return null; + } + + @Override + public Void visit(OracleBlock block, S context) { + analysis.claimTopLevel(); + return super.visit(block, context); + } + + @Override + public Void visit(OracleAssignment assignment, S context) { + analysis.claimTopLevel(); + // Assignment to a local variable is not a database write. + return super.visit(assignment, context); + } + + @Override + public Void visit(OracleNullStatement statement, S context) { + analysis.claimTopLevel(); + return null; + } + + + @Override + public Void visit(CreateAccessMethod statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterCollation statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterRoutine statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterTrigger statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterDatabase statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterSchema statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterStatistics statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterTablespaceMove statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateTablespace statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterTablespace statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterTextSearchConfiguration statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateTextSearchConfiguration statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateUser statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterUser statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(RenameUser statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(SetDefaultRoleStatement statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(SetPasswordStatement statement, S context) { + return schemaOnly(); + } + + // These statements store definitions; visiting their children must not make a stored + // rule action or policy predicate appear to execute during the DDL statement itself. + + @Override + public Void visit(AlterRelation statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterPolicy statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(DropPolicy statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateStatistics statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateForeignDataWrapper statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterForeignDataWrapper statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateServer statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterServer statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateUserMapping statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterUserMapping statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(DropUserMapping statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateRule statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateCollation statement, S context) { + return schemaOnly(); + } + +} diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatures.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatures.java new file mode 100644 index 0000000000..bf80a1b032 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatures.java @@ -0,0 +1,122 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +/*- + * Copyright (C) 2004 - 2026 JSQLParser + * Licensed under the Apache License, Version 2.0. + */ +package net.sf.jsqlparser.statement; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Result of a static feature analysis of a {@link Statement}. + * + *

+ * Every flag is three-valued: certain (the grammar proves it), possible (could + * not be excluded), or absent (the grammar proves it cannot happen). Callers pick their own risk + * direction: + * + *

    + *
  • a dispatcher (executeQuery vs. executeUpdate) uses {@link #is(StmtFeature)}
  • + *
  • a read-only guard uses {@link #may(StmtFeature)}
  • + *
+ * + *

+ * This is a syntactic claim, never a semantic guarantee. Function volatility lives in the + * catalogue (e.g. {@code pg_proc.provolatile}), not in the parse tree. + */ +public final class StatementFeatures { + + private final Set certain; + private final Set uncertain; + private final Set unresolved; + + public StatementFeatures(EnumSet certain, EnumSet uncertain, + Set unresolved) { + // a feature proven certain is never also reported as merely possible + EnumSet u = EnumSet.copyOf(uncertain); + u.removeAll(certain); + this.certain = Collections.unmodifiableSet(EnumSet.copyOf(certain)); + this.uncertain = Collections.unmodifiableSet(u); + this.unresolved = Collections.unmodifiableSet(new LinkedHashSet<>(unresolved)); + } + + /** Proven by the grammar. */ + public boolean is(StmtFeature feature) { + return certain.contains(feature); + } + + /** Proven, or not excludable. This is what a guard must call. */ + public boolean may(StmtFeature feature) { + return certain.contains(feature) || uncertain.contains(feature); + } + + public Set getCertain() { + return certain; + } + + /** Strictly the "could not be excluded" set; disjoint from {@link #getCertain()}. */ + public Set getUncertain() { + return uncertain; + } + + /** + * Why the analysis is uncertain: unresolved function names, dynamic SQL markers, called + * procedure names. Resolve these against your own catalogue or allow-list. + * + *

+ * The generic fallback for an unclassified statement adds no Java type name. It can report + * {@link StmtFeature#OPAQUE} with an empty reference set; an empty set does not prove safety. + * Use {@link #isOpaque()} and {@link #may(StmtFeature)} to inspect the verdict. Explicit + * markers remain, including the statement-type markers for stored routine declarations. + */ + public Set getUnresolvedReferences() { + return unresolved; + } + + public boolean isOpaque() { + return may(StmtFeature.OPAQUE); + } + + // ---- convenience shorthands ------------------------------------------------------------- + // Deliberately named for what they mean, not for the DDL/DML/Query folklore, which cannot + // express "INSERT ... RETURNING *" (both row-returning and mutating) at all. + + public boolean returnsResultSet() { + return is(StmtFeature.RETURNS_RESULT_SET); + } + + public boolean modifiesData() { + return is(StmtFeature.MODIFIES_DATA); + } + + public boolean mayModifyData() { + return may(StmtFeature.MODIFIES_DATA); + } + + public boolean modifiesSchema() { + return is(StmtFeature.MODIFIES_SCHEMA); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder("certain=").append(certain); + if (!uncertain.isEmpty()) { + b.append(" possible=").append(uncertain); + } + if (!unresolved.isEmpty()) { + b.append(" unresolved=").append(unresolved); + } + return b.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 9ebab53a8b..c802107c0b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,19 +9,75 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; +import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterRoutine; +import net.sf.jsqlparser.statement.alter.AlterTrigger; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; +import net.sf.jsqlparser.statement.alter.AlterStatistics; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespace; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.ReassignOwned; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; +import net.sf.jsqlparser.statement.create.type.CreateType; +import net.sf.jsqlparser.statement.alter.AlterType; +import net.sf.jsqlparser.statement.create.domain.CreateDomain; +import net.sf.jsqlparser.statement.alter.AlterDomain; +import net.sf.jsqlparser.statement.create.extension.CreateExtension; +import net.sf.jsqlparser.statement.alter.AlterExtension; +import net.sf.jsqlparser.statement.create.publication.CreatePublication; +import net.sf.jsqlparser.statement.alter.AlterPublication; +import net.sf.jsqlparser.statement.create.subscription.CreateSubscription; +import net.sf.jsqlparser.statement.alter.AlterSubscription; + import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.statement.alter.AlterSession; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; +import net.sf.jsqlparser.statement.alter.AlterUser; +import net.sf.jsqlparser.statement.alter.RenameUser; 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.event.AlterEvent; +import net.sf.jsqlparser.statement.create.event.CreateEvent; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -32,20 +88,37 @@ import net.sf.jsqlparser.statement.grant.Grant; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.insert.InsertBulk; import net.sf.jsqlparser.statement.insert.ParenthesedInsert; import net.sf.jsqlparser.statement.lock.LockStatement; import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; public interface StatementVisitor { + default T visit(InsertBulk statement, S context) { + return null; + } + + default void visit(InsertBulk statement) { + this.visit(statement, null); + } + T visit(Analyze analyze, S context); default void visit(Analyze analyze) { @@ -70,6 +143,22 @@ default void visit(Comment comment) { this.visit(comment, null); } + default T visit(StartTransaction statement, S context) { + return null; + } + + default void visit(StartTransaction statement) { + visit(statement, null); + } + + default T visit(ReleaseSavepointStatement statement, S context) { + return null; + } + + default void visit(ReleaseSavepointStatement statement) { + visit(statement, null); + } + T visit(Commit commit, S context); default void visit(Commit commit) { @@ -100,6 +189,14 @@ default void visit(Drop drop) { this.visit(drop, null); } + default T visit(ReassignOwned statement, S context) { + return null; + } + + default void visit(ReassignOwned statement) { + this.visit(statement, null); + } + T visit(Truncate truncate, S context); default void visit(Truncate truncate) { @@ -118,12 +215,108 @@ default void visit(CreateSchema createSchema) { this.visit(createSchema, null); } + T visit(CreateDatabase createDatabase, S context); + + default void visit(CreateDatabase createDatabase) { + this.visit(createDatabase, null); + } + + T visit(CreateUser createUser, S context); + + default void visit(CreateUser createUser) { + this.visit(createUser, null); + } + + default T visit(AlterUser alterUser, S context) { + return null; + } + + default void visit(AlterUser alterUser) { + this.visit(alterUser, null); + } + + default T visit(RenameUser renameUser, S context) { + return null; + } + + default void visit(RenameUser renameUser) { + this.visit(renameUser, null); + } + + default T visit(SetRoleStatement setRole, S context) { + return null; + } + + default void visit(SetRoleStatement setRole) { + this.visit(setRole, null); + } + + default T visit(SetDefaultRoleStatement setDefaultRole, S context) { + return null; + } + + default void visit(SetDefaultRoleStatement setDefaultRole) { + this.visit(setDefaultRole, null); + } + + default T visit(SetPasswordStatement setPassword, S context) { + return null; + } + + default void visit(SetPasswordStatement setPassword) { + this.visit(setPassword, null); + } + + T visit(CreateEvent createEvent, S context); + + default void visit(CreateEvent createEvent) { + this.visit(createEvent, null); + } + T visit(CreateTable createTable, S context); default void visit(CreateTable createTable) { this.visit(createTable, null); } + T visit(CreateTrigger createTrigger, S context); + + default void visit(CreateTrigger createTrigger) { + this.visit(createTrigger, null); + } + + default T visit(CreateEventTrigger statement, S context) { + return null; + } + + default void visit(CreateEventTrigger statement) { + this.visit(statement, null); + } + + default T visit(AlterEventTrigger statement, S context) { + return null; + } + + default void visit(AlterEventTrigger statement) { + this.visit(statement, null); + } + + default T visit(Reindex statement, S context) { + return null; + } + + default void visit(Reindex statement) { + this.visit(statement, null); + } + + default T visit(SecurityLabel statement, S context) { + return null; + } + + default void visit(SecurityLabel statement) { + this.visit(statement, null); + } + T visit(CreateView createView, S context); default void visit(CreateView createView) { @@ -148,6 +341,12 @@ default void visit(Alter alter) { this.visit(alter, null); } + T visit(AlterEvent alterEvent, S context); + + default void visit(AlterEvent alterEvent) { + this.visit(alterEvent, null); + } + T visit(Statements statements, S context); default void visit(Statements statements) { @@ -156,6 +355,22 @@ default void visit(Statements statements) { T visit(Execute execute, S context); + default T visit(DoStatement statement, S context) { + return null; + } + + default void visit(DoStatement statement) { + this.visit(statement, null); + } + + default T visit(TableMaintenanceStatement statement, S context) { + return null; + } + + default void visit(TableMaintenanceStatement statement) { + this.visit(statement, null); + } + default void visit(Execute execute) { this.visit(execute, null); } @@ -190,6 +405,22 @@ default void visit(ShowTablesStatement showTables) { this.visit(showTables, null); } + default T visit(ShowTableStatusStatement showTableStatus, S context) { + return null; + } + + default void visit(ShowTableStatusStatement showTableStatus) { + this.visit(showTableStatus, null); + } + + default T visit(ShowCreateStatement showCreate, S context) { + return null; + } + + default void visit(ShowCreateStatement showCreate) { + this.visit(showCreate, null); + } + T visit(Merge merge, S context); default void visit(Merge merge) { @@ -208,6 +439,14 @@ default void visit(Upsert upsert) { this.visit(upsert, null); } + default T visit(SetIdentityInsertStatement statement, S context) { + return null; + } + + default void visit(SetIdentityInsertStatement statement) { + visit(statement, null); + } + T visit(UseStatement use, S context); default void visit(UseStatement use) { @@ -292,6 +531,90 @@ default void visit(RenameTableStatement renameTableStatement) { this.visit(renameTableStatement, null); } + T visit(PragmaStatement pragmaStatement, S context); + + default void visit(PragmaStatement pragmaStatement) { + this.visit(pragmaStatement, null); + } + + T visit(ExtensionStatement extensionStatement, S context); + + default void visit(ExtensionStatement extensionStatement) { + this.visit(extensionStatement, null); + } + + T visit(AttachStatement attachStatement, S context); + + default void visit(AttachStatement attachStatement) { + this.visit(attachStatement, null); + } + + T visit(DetachStatement detachStatement, S context); + + default void visit(DetachStatement detachStatement) { + this.visit(detachStatement, null); + } + + T visit(ConnectStatement connectStatement, S context); + + default void visit(ConnectStatement connectStatement) { + this.visit(connectStatement, null); + } + + T visit(DisconnectStatement disconnectStatement, S context); + + default void visit(DisconnectStatement disconnectStatement) { + this.visit(disconnectStatement, null); + } + + T visit(PrepareStatement prepareStatement, S context); + + default void visit(PrepareStatement prepareStatement) { + this.visit(prepareStatement, null); + } + + T visit(DeallocateStatement deallocateStatement, S context); + + default void visit(DeallocateStatement deallocateStatement) { + this.visit(deallocateStatement, null); + } + + T visit(CopyStatement copyStatement, S context); + + default void visit(CopyStatement copyStatement) { + this.visit(copyStatement, null); + } + + T visit(CreateMacro createMacro, S context); + + default void visit(CreateMacro createMacro) { + this.visit(createMacro, null); + } + + T visit(CreateExtensionRepository createExtensionRepository, S context); + + default void visit(CreateExtensionRepository createExtensionRepository) { + this.visit(createExtensionRepository, null); + } + + T visit(AssertStatement assertStatement, S context); + + default void visit(AssertStatement assertStatement) { + this.visit(assertStatement, null); + } + + T visit(ExportDataStatement exportDataStatement, S context); + + default void visit(ExportDataStatement exportDataStatement) { + this.visit(exportDataStatement, null); + } + + T visit(LoadDataStatement loadDataStatement, S context); + + default void visit(LoadDataStatement loadDataStatement) { + this.visit(loadDataStatement, null); + } + T visit(PurgeStatement purgeStatement, S context); default void visit(PurgeStatement purgeStatement) { @@ -358,4 +681,348 @@ default void visit(CreatePolicy createPolicy) { this.visit(createPolicy, null); } + + default T visit(CreateRole statement, S context) { + return null; + } + + default void visit(CreateRole statement) { + visit(statement, null); + } + + default T visit(AlterSchema statement, S context) { + return null; + } + + default void visit(AlterSchema statement) { + visit(statement, null); + } + + default T visit(AlterRole statement, S context) { + return null; + } + + default void visit(AlterRole statement) { + visit(statement, null); + } + + default T visit(Revoke statement, S context) { + return null; + } + + default void visit(Revoke statement) { + visit(statement, null); + } + + default T visit(AlterDefaultPrivileges statement, S context) { + return null; + } + + default void visit(AlterDefaultPrivileges statement) { + visit(statement, null); + } + + default T visit(CreateType statement, S context) { + return null; + } + + default void visit(CreateType statement) { + visit(statement, null); + } + + default T visit(AlterType statement, S context) { + return null; + } + + default void visit(AlterType statement) { + visit(statement, null); + } + + default T visit(CreateDomain statement, S context) { + return null; + } + + default void visit(CreateDomain statement) { + visit(statement, null); + } + + default T visit(AlterDomain statement, S context) { + return null; + } + + default void visit(AlterDomain statement) { + visit(statement, null); + } + + default T visit(CreateExtension statement, S context) { + return null; + } + + default void visit(CreateExtension statement) { + visit(statement, null); + } + + default T visit(AlterExtension statement, S context) { + return null; + } + + default void visit(AlterExtension statement) { + visit(statement, null); + } + + default T visit(CreatePublication statement, S context) { + return null; + } + + default void visit(CreatePublication statement) { + visit(statement, null); + } + + default T visit(AlterPublication statement, S context) { + return null; + } + + default void visit(AlterPublication statement) { + visit(statement, null); + } + + default T visit(CreateSubscription statement, S context) { + return null; + } + + default void visit(CreateSubscription statement) { + visit(statement, null); + } + + default T visit(AlterSubscription statement, S context) { + return null; + } + + default void visit(AlterSubscription statement) { + visit(statement, null); + } + + default T visit(OracleBlock block, S context) { + return visit((Block) block, context); + } + + default void visit(OracleBlock block) { + visit(block, null); + } + + default T visit(OracleAssignment assignment, S context) { + return null; + } + + default void visit(OracleAssignment assignment) { + visit(assignment, null); + } + + default T visit(OracleNullStatement statement, S context) { + return null; + } + + default void visit(OracleNullStatement statement) { + visit(statement, null); + } + + default T visit(AlterRelation statement, S context) { + return null; + } + + default void visit(AlterRelation statement) { + visit(statement, null); + } + + default T visit(AlterDatabase statement, S context) { + return null; + } + + default void visit(AlterDatabase statement) { + visit(statement, null); + } + + default T visit(AlterTablespaceMove statement, S context) { + return null; + } + + default void visit(AlterTablespaceMove statement) { + visit(statement, null); + } + + default T visit(CreateTablespace statement, S context) { + return null; + } + + default void visit(CreateTablespace statement) { + visit(statement, null); + } + + default T visit(AlterTablespace statement, S context) { + return null; + } + + default void visit(AlterTablespace statement) { + visit(statement, null); + } + + default T visit(AlterPolicy statement, S context) { + return null; + } + + default void visit(AlterPolicy statement) { + visit(statement, null); + } + + default T visit(DropPolicy statement, S context) { + return null; + } + + default void visit(DropPolicy statement) { + visit(statement, null); + } + + default T visit(CreateStatistics statement, S context) { + return null; + } + + default void visit(CreateStatistics statement) { + visit(statement, null); + } + + default T visit(AlterStatistics statement, S context) { + return null; + } + + default void visit(AlterStatistics statement) { + visit(statement, null); + } + + default T visit(CreateForeignDataWrapper statement, S context) { + return null; + } + + default void visit(CreateForeignDataWrapper statement) { + visit(statement, null); + } + + default T visit(AlterForeignDataWrapper statement, S context) { + return null; + } + + default void visit(AlterForeignDataWrapper statement) { + visit(statement, null); + } + + default T visit(CreateServer statement, S context) { + return null; + } + + default void visit(CreateServer statement) { + visit(statement, null); + } + + default T visit(AlterServer statement, S context) { + return null; + } + + default void visit(AlterServer statement) { + visit(statement, null); + } + + default T visit(CreateUserMapping statement, S context) { + return null; + } + + default void visit(CreateUserMapping statement) { + visit(statement, null); + } + + default T visit(AlterUserMapping statement, S context) { + return null; + } + + default void visit(AlterUserMapping statement) { + visit(statement, null); + } + + default T visit(DropUserMapping statement, S context) { + return null; + } + + default void visit(DropUserMapping statement) { + visit(statement, null); + } + + default T visit(CreateAccessMethod statement, S context) { + return null; + } + + default void visit(CreateAccessMethod statement) { + visit(statement, null); + } + + default T visit(CreateTextSearchConfiguration statement, S context) { + return null; + } + + default void visit(CreateTextSearchConfiguration statement) { + visit(statement, null); + } + + default T visit(AlterTextSearchConfiguration statement, S context) { + return null; + } + + default void visit(AlterTextSearchConfiguration statement) { + visit(statement, null); + } + + default T visit(CreateRule statement, S context) { + return null; + } + + default void visit(CreateRule statement) { + visit(statement, null); + } + + default T visit(NotifyStatement statement, S context) { + return null; + } + + default void visit(NotifyStatement statement) { + visit(statement, null); + } + + default T visit(CreateCollation statement, S context) { + return null; + } + + default void visit(CreateCollation statement) { + visit(statement, null); + } + + default T visit(AlterCollation statement, S context) { + return null; + } + + default void visit(AlterCollation statement) { + visit(statement, null); + } + + default T visit(AlterRoutine statement, S context) { + return null; + } + + default void visit(AlterRoutine statement) { + visit(statement, null); + } + + default T visit(AlterTrigger statement, S context) { + return null; + } + + default void visit(AlterTrigger statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 3b12c01c0b..5367cc9a0c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -9,23 +9,77 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespace; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.alter.AlterRoutine; +import net.sf.jsqlparser.statement.alter.AlterTrigger; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; + +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; + +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.ReassignOwned; +import net.sf.jsqlparser.statement.role.RoleOption; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; +import net.sf.jsqlparser.statement.create.type.CreateType; +import net.sf.jsqlparser.statement.alter.AlterType; +import net.sf.jsqlparser.statement.create.domain.CreateDomain; +import net.sf.jsqlparser.statement.alter.AlterDomain; +import net.sf.jsqlparser.statement.create.extension.CreateExtension; +import net.sf.jsqlparser.statement.alter.AlterExtension; +import net.sf.jsqlparser.statement.create.publication.CreatePublication; +import net.sf.jsqlparser.statement.alter.AlterPublication; +import net.sf.jsqlparser.statement.create.subscription.CreateSubscription; +import net.sf.jsqlparser.statement.alter.AlterSubscription; + +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.schema.Partition; import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; import net.sf.jsqlparser.statement.alter.AlterSession; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; +import net.sf.jsqlparser.statement.alter.AlterUser; +import net.sf.jsqlparser.statement.alter.RenameUser; 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.event.AlterEvent; +import net.sf.jsqlparser.statement.create.event.CreateEvent; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -36,6 +90,7 @@ import net.sf.jsqlparser.statement.grant.Grant; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.insert.InsertBulk; import net.sf.jsqlparser.statement.insert.InsertConflictAction; import net.sf.jsqlparser.statement.insert.ParenthesedInsert; import net.sf.jsqlparser.statement.lock.LockStatement; @@ -43,6 +98,8 @@ import net.sf.jsqlparser.statement.merge.MergeOperationVisitor; import net.sf.jsqlparser.statement.merge.MergeOperationVisitorAdapter; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.FromItemVisitor; import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter; import net.sf.jsqlparser.statement.select.PivotVisitor; @@ -53,7 +110,9 @@ import net.sf.jsqlparser.statement.select.SelectVisitor; import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; @@ -61,6 +120,11 @@ import net.sf.jsqlparser.statement.upsert.Upsert; import java.util.List; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) public class StatementVisitorAdapter implements StatementVisitor { @@ -71,14 +135,46 @@ public class StatementVisitorAdapter implements StatementVisitor { private final SelectVisitor selectVisitor; private final MergeOperationVisitor mergeOperationVisitor; + /** + * Builds a fully connected visitor graph. + * + *

+ * The sub-visitors are mutually recursive, so none of them can be handed its collaborators at + * construction time; the cycle has to be closed afterwards through the setters. The previous + * version skipped that step and the graph was not actually connected: + * + *

    + *
  • {@code new ExpressionVisitorAdapter<>()} leaves its {@code selectVisitor} null, and + * {@code ExpressionVisitorAdapter#visit(Select)} is a no-op when it is null - so every + * sub-select in an expression ({@code WHERE id IN (SELECT ..)}, scalar subqueries, EXISTS) was + * silently dropped;
  • + *
  • {@code new FromItemVisitorAdapter<>()} builds its own throwaway SelectVisitorAdapter and + * ExpressionVisitorAdapter, so subqueries in FROM were traversed by a detached chain that no + * subclass of this adapter could observe.
  • + *
+ * + *

+ * Both meant a subclass overriding a visit method never saw the nested nodes. The three setter + * calls below fix that; {@code setStatementVisitor} additionally lets the select visitor hand a + * data-modifying CTE body back here. + */ public StatementVisitorAdapter() { - this.expressionVisitor = new ExpressionVisitorAdapter<>(); - this.pivotVisitor = new PivotVisitorAdapter<>(this.expressionVisitor); - this.selectItemVisitor = new SelectItemVisitorAdapter<>(this.expressionVisitor); - this.fromItemVisitor = new FromItemVisitorAdapter<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter<>(); + FromItemVisitorAdapter fromItems = new FromItemVisitorAdapter<>(); - this.selectVisitor = new SelectVisitorAdapter<>(this.expressionVisitor, this.pivotVisitor, - this.selectItemVisitor, this.fromItemVisitor); + this.expressionVisitor = expressions; + this.pivotVisitor = new PivotVisitorAdapter<>(expressions); + this.selectItemVisitor = new SelectItemVisitorAdapter<>(expressions); + this.fromItemVisitor = fromItems; + + SelectVisitorAdapter selects = new SelectVisitorAdapter<>(expressions, this.pivotVisitor, + this.selectItemVisitor, fromItems); + + expressions.setSelectVisitor(selects); + fromItems.setSelectVisitor((SelectVisitor) selects).setExpressionVisitor(expressions); + selects.setStatementVisitor(this); + + this.selectVisitor = selects; this.mergeOperationVisitor = new MergeOperationVisitorAdapter<>(); } @@ -92,10 +188,12 @@ public StatementVisitorAdapter(ExpressionVisitor expressionVisitor, this.fromItemVisitor = fromItemVisitor; this.selectVisitor = selectVisitor; this.mergeOperationVisitor = mergeOperationVisitor; + adoptAsStatementVisitor(selectVisitor); } public StatementVisitorAdapter(SelectVisitorAdapter selectVisitor) { this.selectVisitor = selectVisitor; + adoptAsStatementVisitor(selectVisitor); this.expressionVisitor = selectVisitor.getExpressionVisitor(); this.pivotVisitor = selectVisitor.getPivotVisitor(); this.selectItemVisitor = selectVisitor.getSelectItemVisitor(); @@ -103,9 +201,37 @@ public StatementVisitorAdapter(SelectVisitorAdapter selectVisitor) { this.mergeOperationVisitor = new MergeOperationVisitorAdapter<>(); } + /** + * Lets the select visitor hand a data-modifying CTE body back to this statement visitor. + * Without it, {@code SelectVisitorAdapter#visit(WithItem)} has no way to traverse + * {@code WITH c AS (DELETE FROM t RETURNING *) ..} and silently skips it. + * + *

+ * An explicitly configured statement visitor is never overwritten. + */ + private void adoptAsStatementVisitor(SelectVisitor visitor) { + if (visitor instanceof SelectVisitorAdapter) { + SelectVisitorAdapter adapter = (SelectVisitorAdapter) visitor; + if (adapter.getStatementVisitor() == null) { + adapter.setStatementVisitor(this); + } + } + } + @Override public T visit(Comment comment, S context) { + comment.visitRelations(table -> table.accept(fromItemVisitor, context)); + expressionVisitor.visitExpression(comment.getComment(), context); + return null; + } + @Override + public T visit(StartTransaction statement, S context) { + return null; + } + + @Override + public T visit(ReleaseSavepointStatement statement, S context) { return null; } @@ -136,6 +262,10 @@ public T visit(Delete delete, S context) { expressionVisitor.visitLimit(delete.getLimit(), context); + // was missing, unlike visit(Insert) and visit(Update): + // DELETE FROM t RETURNING f(x) left f(x) untraversed + visitReturningClause(delete.getReturningClause(), context); + return null; } @@ -204,7 +334,7 @@ public T visit(Insert insert, S context) { selectVisitor.visitOutputClause(insert.getOutputClause(), context); if (insert.getSelect() != null) { - insert.getSelect().accept(selectVisitor, null); + insert.getSelect().accept(selectVisitor, context); } expressionVisitor.visitUpdateSets(insert.getSetUpdateSets(), context); @@ -217,6 +347,9 @@ public T visit(Insert insert, S context) { expressionVisitor.visitUpdateSets(conflictAction.getUpdateSets(), context); } + if (insert.getConflictTarget() != null) { + insert.getConflictTarget().accept(expressionVisitor, context); + } visitReturningClause(insert.getReturningClause(), context); return null; } @@ -229,6 +362,13 @@ private T visitReturningClause(ReturningClause returningClause, S context) { return null; } + @Override + public T visit(InsertBulk statement, S context) { + fromItemVisitor.visitFromItem(statement.getTable(), context); + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + @Override public T visit(ParenthesedInsert insert, S context) { insert.getInsert().accept(this, context); @@ -237,14 +377,16 @@ public T visit(ParenthesedInsert insert, S context) { @Override public T visit(Drop drop, S context) { - if (drop.getType().equalsIgnoreCase("table")) { - fromItemVisitor.visitFromItem(drop.getName(), context); - } - // @todo: handle schemas + drop.visitTables(table -> fromItemVisitor.visitFromItem(table, context)); return null; } + @Override + public T visit(ReassignOwned statement, S context) { + return null; + } + @Override public T visit(Truncate truncate, S context) { return truncate.getTable().accept(fromItemVisitor, context); @@ -252,28 +394,127 @@ public T visit(Truncate truncate, S context) { @Override public T visit(CreateIndex createIndex, S context) { - + TableDefinitionTraversal.visit(createIndex, + expression -> expression.accept(expressionVisitor, context), + table -> table.accept(fromItemVisitor, context)); return null; } @Override public T visit(CreateSchema createSchema, S context) { + createSchema.getStatements().forEach(element -> element.accept(this, context)); + return null; + } + + @Override + public T visit(CreateDatabase createDatabase, S context) { + return null; + } + + @Override + public T visit(CreateUser createUser, S context) { + return null; + } + + @Override + public T visit(AlterUser alterUser, S context) { + return null; + } + + @Override + public T visit(RenameUser renameUser, S context) { + return null; + } + + @Override + public T visit(SetRoleStatement setRole, S context) { + return null; + } + + @Override + public T visit(SetDefaultRoleStatement setDefaultRole, S context) { + return null; + } + + @Override + public T visit(SetPasswordStatement setPassword, S context) { + return null; + } + + @Override + public T visit(CreateEvent createEvent, S context) { return null; } @Override public T visit(CreateTable createTable, S context) { + TableDefinitionTraversal.visit(createTable, + expression -> expression.accept(expressionVisitor, context), + table -> table.accept(fromItemVisitor, context)); + if (createTable.getSelect() != null) { + createTable.getSelect().accept(selectVisitor, context); + } return createTable.getTable().accept(fromItemVisitor, context); } + @Override + public T visit(CreateTrigger createTrigger, S context) { + createTrigger.visit(t -> t.accept(fromItemVisitor, context), + e -> e.accept(expressionVisitor, context)); + if (createTrigger.getBody() != null) { + createTrigger.getBody().accept(this, context); + } + return null; + } + + @Override + public T visit(CreateEventTrigger statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(AlterEventTrigger statement, S context) { + return null; + } + + @Override + public T visit(Reindex statement, S context) { + if (statement.getTable() != null) { + statement.getTable().accept(fromItemVisitor, context); + } + return null; + } + + @Override + public T visit(SecurityLabel statement, S context) { + statement.visitRelations(table -> table.accept(fromItemVisitor, context)); + expressionVisitor.visitExpression(statement.getLabel(), context); + return null; + } + @Override public T visit(CreateView createView, S context) { + createView.getView().accept(fromItemVisitor, context); + if (createView.getSelect() != null) { + createView.getSelect().accept(selectVisitor, context); + } return null; } @Override public T visit(Alter alter, S context) { + alter.getTable().accept(fromItemVisitor, context); + for (AlterExpression action : alter.getAlterExpressions()) { + TableDefinitionTraversal.visit(action, + expression -> expression.accept(expressionVisitor, context), + table -> table.accept(fromItemVisitor, context)); + } + return null; + } + @Override + public T visit(AlterEvent alterEvent, S context) { return null; } @@ -287,7 +528,13 @@ public T visit(Statements statements, S context) { @Override public T visit(Execute execute, S context) { + expressionVisitor.visitExpression(execute.getExprList(), context); + return null; + } + @Override + public T visit(DoStatement statement, S context) { + expressionVisitor.visitExpression(statement.getCode(), context); return null; } @@ -299,13 +546,22 @@ public T visit(LockStatement lock, S context) { @Override public T visit(CreatePolicy createPolicy, S context) { + fromItemVisitor.visitFromItem(createPolicy.getTable(), context); + expressionVisitor.visitExpression(createPolicy.getUsingExpression(), context); + expressionVisitor.visitExpression(createPolicy.getWithCheckExpression(), context); return null; } @Override public T visit(SetStatement set, S context) { - + for (int i = 0; i < set.getCount(); i++) { + if (set.getExpressions(i) != null) { + for (Expression expression : set.getExpressions(i)) { + expressionVisitor.visitExpression(expression, context); + } + } + } return null; } @@ -323,6 +579,7 @@ public T visit(Merge merge, S context) { expressionVisitor.visitExpression(merge.getOnCondition(), context); mergeOperationVisitor.visit(merge.getOperations(), context); selectVisitor.visitOutputClause(merge.getOutputClause(), context); + visitReturningClause(merge.getReturningClause(), context); return null; } @@ -336,6 +593,11 @@ public T visit(Upsert upsert, S context) { return null; } + @Override + public T visit(SetIdentityInsertStatement statement, S context) { + return statement.getTable().accept(fromItemVisitor, context); + } + @Override public T visit(UseStatement use, S context) { return null; @@ -376,6 +638,16 @@ public T visit(ShowTablesStatement showTables, S context) { return null; } + @Override + public T visit(ShowTableStatusStatement showTableStatus, S context) { + return null; + } + + @Override + public T visit(ShowCreateStatement showCreate, S context) { + return null; + } + @Override public T visit(DeclareStatement declareStatement, S context) { return null; @@ -383,21 +655,40 @@ public T visit(DeclareStatement declareStatement, S context) { @Override public T visit(Grant grant, S context) { + grant.getClause().visit(t -> t.accept(fromItemVisitor, context), + e -> e.accept(expressionVisitor, context)); return null; } @Override public T visit(CreateSequence createSequence, S context) { + if (createSequence.getSequence().getOwnership() != null + && createSequence.getSequence().getOwnership().getColumn() != null) { + createSequence.getSequence().getOwnership().getColumn().accept(expressionVisitor, + context); + } return null; } @Override public T visit(AlterSequence alterSequence, S context) { + if (alterSequence.getSequence().getOwnership() != null + && alterSequence.getSequence().getOwnership().getColumn() != null) { + alterSequence.getSequence().getOwnership().getColumn().accept(expressionVisitor, + context); + } return null; } @Override public T visit(CreateFunctionalStatement createFunctionalStatement, S context) { + if (createFunctionalStatement.getReturnType() != null + && createFunctionalStatement.getReturnType().getTableElements() != null) { + createFunctionalStatement.getReturnType().getTableElements() + .forEach(element -> TableDefinitionTraversal.visit(element, + expression -> expression.accept(expressionVisitor, context), + table -> table.accept(fromItemVisitor, context))); + } return null; } @@ -412,6 +703,11 @@ public T visit(Analyze analyze, S context) { return null; } + @Override + public T visit(TableMaintenanceStatement statement, S context) { + return null; + } + @Override public T visit(SavepointStatement savepointStatement, S context) { // @todo: do something usefully here @@ -444,6 +740,76 @@ public T visit(RenameTableStatement renameTableStatement, S context) { return null; } + @Override + public T visit(PragmaStatement pragmaStatement, S context) { + return null; + } + + @Override + public T visit(AssertStatement assertStatement, S context) { + return null; + } + + @Override + public T visit(ExtensionStatement extensionStatement, S context) { + return null; + } + + @Override + public T visit(ExportDataStatement exportDataStatement, S context) { + return null; + } + + @Override + public T visit(AttachStatement attachStatement, S context) { + return null; + } + + @Override + public T visit(DetachStatement detachStatement, S context) { + return null; + } + + @Override + public T visit(ConnectStatement connectStatement, S context) { + return null; + } + + @Override + public T visit(DisconnectStatement disconnectStatement, S context) { + return null; + } + + @Override + public T visit(PrepareStatement prepareStatement, S context) { + return null; + } + + @Override + public T visit(DeallocateStatement deallocateStatement, S context) { + return null; + } + + @Override + public T visit(CopyStatement copyStatement, S context) { + return null; + } + + @Override + public T visit(CreateMacro createMacro, S context) { + return null; + } + + @Override + public T visit(CreateExtensionRepository createExtensionRepository, S context) { + return null; + } + + @Override + public T visit(LoadDataStatement loadDataStatement, S context) { + return null; + } + @Override public T visit(PurgeStatement purgeStatement, S context) { return null; @@ -474,4 +840,289 @@ public T visit(Import imprt, S context) { public T visit(Export export, S context) { return null; } + + @Override + public T visit(CreateRole statement, S context) { + for (RoleOption option : statement.getOptions()) { + if (option.getValue() != null) { + option.getValue().accept(expressionVisitor, context); + } + } + return null; + } + + @Override + public T visit(AlterRole statement, S context) { + for (RoleOption option : statement.getOptions()) { + if (option.getValue() != null) { + option.getValue().accept(expressionVisitor, context); + } + } + if (statement.getValues() != null) { + statement.getValues().forEach(e -> e.accept(expressionVisitor, context)); + } + return null; + } + + @Override + public T visit(Revoke statement, S context) { + statement.getClause().visit(t -> t.accept(fromItemVisitor, context), + e -> e.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(AlterDefaultPrivileges statement, S context) { + if (statement.getGrant() != null) { + statement.getGrant().accept(this, context); + } + if (statement.getRevoke() != null) { + statement.getRevoke().accept(this, context); + } + return null; + } + + @Override + public T visit(CreateType statement, S context) { + + return null; + } + + @Override + public T visit(AlterType statement, S context) { + + return null; + } + + @Override + public T visit(CreateDomain statement, S context) { + if (statement.getDefaultExpression() != null) { + statement.getDefaultExpression().accept(expressionVisitor, context); + } + statement.getConstraints().forEach(constraint -> { + if (constraint.getExpression() != null) { + constraint.getExpression().accept(expressionVisitor, context); + } + }); + return null; + } + + @Override + public T visit(AlterDomain statement, S context) { + if (statement.getDefaultExpression() != null) { + statement.getDefaultExpression().accept(expressionVisitor, context); + } + if (statement.getConstraint() != null + && statement.getConstraint().getExpression() != null) { + statement.getConstraint().getExpression().accept(expressionVisitor, context); + } + return null; + } + + @Override + public T visit(CreateExtension statement, S context) { + + return null; + } + + @Override + public T visit(AlterExtension statement, S context) { + if (statement.getMember() != null && statement.getMember().isTable()) { + new Table(statement.getMember().getName()) + .accept(fromItemVisitor, context); + } + return null; + } + + @Override + public T visit(CreatePublication statement, S context) { + statement.getTargets().forEach(target -> target.visit( + table -> table.accept(fromItemVisitor, context), + expression -> expression.accept(expressionVisitor, context))); + statement.getOptions().forEach(option -> { + if (option.getValue() != null) { + option.getValue().accept(expressionVisitor, context); + } + }); + return null; + } + + @Override + public T visit(AlterPublication statement, S context) { + statement.getTargets().forEach(target -> target.visit( + table -> table.accept(fromItemVisitor, context), + expression -> expression.accept(expressionVisitor, context))); + statement.getOptions().forEach(option -> { + if (option.getValue() != null) { + option.getValue().accept(expressionVisitor, context); + } + }); + return null; + } + + @Override + public T visit(CreateSubscription statement, S context) { + if (statement.getConnection() != null) { + statement.getConnection().accept(expressionVisitor, context); + } + statement.getOptions().forEach(option -> { + if (option.getValue() != null) { + option.getValue().accept(expressionVisitor, context); + } + }); + return null; + } + + @Override + public T visit(AlterSubscription statement, S context) { + if (statement.getConnection() != null) { + statement.getConnection().accept(expressionVisitor, context); + } + statement.getOptions().forEach(option -> { + if (option.getValue() != null) { + option.getValue().accept(expressionVisitor, context); + } + }); + return null; + } + + @Override + public T visit(OracleBlock block, S context) { + block.visitChildren(expression -> expression.accept(expressionVisitor, context), + statement -> statement.accept(this, context)); + return null; + } + + @Override + public T visit(OracleAssignment assignment, S context) { + assignment.getTarget().accept(expressionVisitor, context); + assignment.getValue().accept(expressionVisitor, context); + return null; + } + + + @Override + public T visit(CreateAccessMethod statement, S context) { + return null; + } + + @Override + public T visit(AlterRelation statement, S context) { + if (statement.getObjectType() != AlterRelation.ObjectType.INDEX) { + fromItemVisitor.visitFromItem(statement.getRelation(), context); + } + statement.getActions().forEach(action -> { + action.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + action.visitTables(table -> table.accept(fromItemVisitor, context)); + }); + return null; + } + + @Override + public T visit(CreateTablespace statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(AlterTablespace statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(AlterPolicy statement, S context) { + fromItemVisitor.visitFromItem(statement.getTable(), context); + if (statement.getNewName() == null) { + statement.getOptions() + .visitExpressions(expression -> expression.accept(expressionVisitor, context)); + } + return null; + } + + @Override + public T visit(DropPolicy statement, S context) { + fromItemVisitor.visitFromItem(statement.getTable(), context); + return null; + } + + @Override + public T visit(CreateStatistics statement, S context) { + fromItemVisitor.visitFromItem(statement.getTable(), context); + expressionVisitor.visitExpression(statement.getExpressions(), context); + return null; + } + + private T visitForeignDataStatement(ForeignDataStatement statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(CreateForeignDataWrapper statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(AlterForeignDataWrapper statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(CreateServer statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(AlterServer statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(CreateUserMapping statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(AlterUserMapping statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(DropUserMapping statement, S context) { + return null; + } + + @Override + public T visit(CreateRule statement, S context) { + statement.visitTables(table -> table.accept(fromItemVisitor, context)); + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + statement.getActions().forEach(action -> action.accept(this, context)); + return null; + } + + @Override + public T visit(NotifyStatement statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(CreateCollation statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(AlterRoutine statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(AlterTrigger statement, S context) { + fromItemVisitor.visitFromItem(statement.getTable(), context); + return null; + } + } diff --git a/src/main/java/net/sf/jsqlparser/statement/Statements.java b/src/main/java/net/sf/jsqlparser/statement/Statements.java index 0c8571c357..b4d6e4f09d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/Statements.java +++ b/src/main/java/net/sf/jsqlparser/statement/Statements.java @@ -12,6 +12,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; public class Statements extends ArrayList implements Serializable { @@ -34,17 +35,30 @@ public E get(Class type, int index) { return type.cast(get(index)); } - @Override - public String toString() { - StringBuilder b = new StringBuilder(); + /** + * Appends each statement using the supplied renderer and the list's separators. Blocks and + * IF/ELSE statements retain their own semicolon settings. Null entries produced by error + * recovery retain the legacy "null;" placeholder without invoking the renderer. + */ + public StringBuilder appendTo(StringBuilder builder, Consumer statementPrinter) { for (Statement stmt : this) { - // IfElseStatements and Blocks control the Semicolons by themselves - if (stmt instanceof IfElseStatement || stmt instanceof Block) { - b.append(stmt).append("\n"); + if (stmt == null) { + builder.append("null"); } else { - b.append(stmt).append(";\n"); + statementPrinter.accept(stmt); + } + // IfElseStatements and Blocks control the Semicolons by themselves + if (!(stmt instanceof IfElseStatement || stmt instanceof Block)) { + builder.append(';'); } + builder.append('\n'); } - return b.toString(); + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StmtFeature.java b/src/main/java/net/sf/jsqlparser/statement/StmtFeature.java new file mode 100644 index 0000000000..00a413851f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/StmtFeature.java @@ -0,0 +1,50 @@ +/*- + * #%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.statement; + +/** + * What a statement does, as far as the grammar can prove it. + * + *

+ * Named {@code StmtFeature} rather than {@code Feature} because + * {@code net.sf.jsqlparser.parser.feature.Feature} already occupies that simple name and the two + * are unrelated: that one describes parser capabilities, this one describes statement effects. + * + *

+ * These are not mutually exclusive. {@code INSERT .. RETURNING *} carries {@link #MODIFIES_DATA} + * and {@link #RETURNS_RESULT_SET}; {@code CREATE TABLE .. AS SELECT} carries + * {@link #MODIFIES_SCHEMA} and {@link #READS_DATA}. Any attempt to collapse them into a single + * Query/DDL/DML verdict loses information the caller needs. + */ +public enum StmtFeature { + /** Reads persistent data - a table or view in a query position. */ + READS_DATA, + + /** + * Returns a result set to the client. A property of the statement's own result + * position, never of a nested one: {@code INSERT INTO x SELECT ..} does not have it. + */ + RETURNS_RESULT_SET, + + /** Inserts, updates, deletes, merges or otherwise destroys rows. */ + MODIFIES_DATA, + + /** Creates, alters, drops, truncates, renames, comments, grants. */ + MODIFIES_SCHEMA, + + /** SET, RESET, USE, DECLARE, ALTER SESSION. */ + MODIFIES_SESSION, + + /** COMMIT, ROLLBACK, SAVEPOINT, LOCK, SELECT .. FOR UPDATE. */ + MODIFIES_TRANSACTION, + + /** Nothing further is knowable statically: CALL, EXECUTE, dynamic SQL, procedure bodies. */ + OPAQUE +} diff --git a/src/main/java/net/sf/jsqlparser/statement/TableMaintenanceStatement.java b/src/main/java/net/sf/jsqlparser/statement/TableMaintenanceStatement.java new file mode 100644 index 0000000000..4c22dad751 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/TableMaintenanceStatement.java @@ -0,0 +1,189 @@ +/*- + * #%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.statement; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Table; + +/** + * A MySQL/MariaDB {@code CHECK TABLE}, {@code CHECKSUM TABLE}, {@code OPTIMIZE TABLE} or + * {@code REPAIR TABLE} statement. {@code ANALYZE TABLE} remains + * {@link net.sf.jsqlparser.statement.analyze.Analyze}. + */ +public class TableMaintenanceStatement extends ASTNodeAccessImpl implements Statement { + + public enum Operation { + CHECK, CHECKSUM, OPTIMIZE, REPAIR; + + /** Whether this operation accepts the option after its table list. */ + public boolean allows(Option option) { + switch (this) { + case CHECK: + return option != Option.USE_FRM; + case CHECKSUM: + return option == Option.QUICK || option == Option.EXTENDED; + case REPAIR: + return option == Option.QUICK || option == Option.EXTENDED + || option == Option.USE_FRM; + default: + return false; + } + } + + /** Whether this operation accepts {@code NO_WRITE_TO_BINLOG} or {@code LOCAL}. */ + public boolean allowsModifier() { + return this == OPTIMIZE || this == REPAIR; + } + } + + public enum Modifier { + NO_WRITE_TO_BINLOG, LOCAL + } + + public enum Option { + FOR_UPGRADE, QUICK, FAST, MEDIUM, EXTENDED, CHANGED, USE_FRM; + + /** Returns the SQL spelling, for example {@code FOR UPGRADE}. */ + public String getKeyword() { + return this == FOR_UPGRADE ? "FOR UPGRADE" : name(); + } + + /** Returns the single-word option spelled by {@code word}, or null. */ + public static Option fromKeyword(String word) { + for (Option option : values()) { + if (option != FOR_UPGRADE && option.name().equalsIgnoreCase(word)) { + return option; + } + } + return null; + } + } + + private Operation operation; + private Modifier modifier; + private boolean tablesKeyword; + private final List

tables = new ArrayList<>(); + private final List
getTables() { + return Collections.unmodifiableList(tables); + } + + public void setTables(Collection tables) { + this.tables.clear(); + if (tables != null) { + this.tables.addAll(tables); + } + } + + /** Returns the options in source order; repeated options are retained as written. */ + public List
visitor) { + if (kind == Kind.INHERIT && relation != null) { + visitor.accept(relation); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/RenameTableStatement.java b/src/main/java/net/sf/jsqlparser/statement/alter/RenameTableStatement.java index 94eb39be0c..1b4537c513 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/RenameTableStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/RenameTableStatement.java @@ -16,6 +16,7 @@ import java.util.Objects; import java.util.Set; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; @@ -25,7 +26,7 @@ * @see Rename */ -public class RenameTableStatement implements Statement { +public class RenameTableStatement extends ASTNodeAccessImpl implements Statement { private final LinkedHashMap tableNames = new LinkedHashMap<>(); private boolean usingTableKeyword = false; @@ -125,8 +126,9 @@ public StringBuilder appendTo(StringBuilder builder) { if (i == 0) { builder .append("RENAME") - .append(usingTableKeyword ? " TABLE " : " ") - .append(usingIfExistsKeyword ? " IF EXISTS " : " ") + .append(usingTableKeyword ? " TABLE" : "") + .append(usingIfExistsKeyword ? " IF EXISTS" : "") + .append(' ') .append(e.getKey()) .append(waitDirective != null && waitDirective.length() > 0 ? " " + waitDirective diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/RenameUser.java b/src/main/java/net/sf/jsqlparser/statement/alter/RenameUser.java new file mode 100644 index 0000000000..77c694cdd0 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/RenameUser.java @@ -0,0 +1,96 @@ +/*- + * #%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.statement.alter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.user.UserAccount; + +/** MySQL {@code RENAME USER old TO new [, old TO new] ...} statement. */ +public class RenameUser extends ASTNodeAccessImpl implements Statement { + + private final List renames = new ArrayList<>(); + + /** One {@code old TO new} pair. */ + public static class Rename implements Serializable { + private UserAccount from; + private UserAccount to; + + public Rename() { + // empty constructor + } + + public Rename(UserAccount from, UserAccount to) { + this.from = from; + this.to = to; + } + + public UserAccount getFrom() { + return from; + } + + public void setFrom(UserAccount from) { + this.from = from; + } + + public UserAccount getTo() { + return to; + } + + public void setTo(UserAccount to) { + this.to = to; + } + + @Override + public String toString() { + return from + " TO " + to; + } + } + + /** Returns the renames in source order. */ + public List getRenames() { + return Collections.unmodifiableList(renames); + } + + public void setRenames(Collection renames) { + this.renames.clear(); + if (renames != null) { + this.renames.addAll(renames); + } + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + @Override + public String toString() { + return "RENAME USER " + + renames.stream().map(Rename::toString).collect(Collectors.joining(", ")); + } + + public RenameUser withRenames(Collection renames) { + setRenames(renames); + return this; + } + + public RenameUser addRename(UserAccount from, UserAccount to) { + renames.add(new Rename(from, to)); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/database/AlterDatabase.java b/src/main/java/net/sf/jsqlparser/statement/alter/database/AlterDatabase.java new file mode 100644 index 0000000000..c01ab3409f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/database/AlterDatabase.java @@ -0,0 +1,196 @@ +/*- + * #%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.statement.alter.database; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.ConfigurationParameterChange; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.database.DatabaseOption; + +/** + * MySQL or PostgreSQL {@code ALTER DATABASE}. MySQL's {@code ALTER SCHEMA} synonym only changes + * options, and a missing name selects the current database. The options use the same + * {@link DatabaseOption} model as {@code CREATE DATABASE}. Names retain SQL identifier quoting. + */ +public class AlterDatabase extends ASTNodeAccessImpl implements Statement { + + public enum Action { + /** MySQL options or PostgreSQL {@code [WITH] option ...}, see {@link #getOptions()}. */ + OPTIONS, + /** {@code RENAME TO}, see {@link #getNewName()}. */ + RENAME, + /** {@code OWNER TO}, see {@link #getOwner()}. */ + OWNER, + /** {@code SET TABLESPACE}, see {@link #getTablespace()}. */ + SET_TABLESPACE, + /** {@code REFRESH COLLATION VERSION}. */ + REFRESH_COLLATION_VERSION, + /** {@code SET} or {@code RESET}, see {@link #getParameterChange()}. */ + CONFIGURATION_PARAMETER + } + + private String databaseName; + private boolean useSchemaKeyword; + private Action action = Action.OPTIONS; + private boolean useWith; + private final List options = new ArrayList<>(); + private String newName; + private String owner; + private String tablespace; + private ConfigurationParameterChange parameterChange; + + public String getDatabaseName() { + return databaseName; + } + + public void setDatabaseName(String databaseName) { + this.databaseName = databaseName; + } + + public boolean isUseSchemaKeyword() { + return useSchemaKeyword; + } + + public void setUseSchemaKeyword(boolean useSchemaKeyword) { + this.useSchemaKeyword = useSchemaKeyword; + } + + /** + * The rendered alteration. Payloads of other actions are retained for a later switch back. + * + * @return the action, {@link Action#OPTIONS} unless changed + */ + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + /** + * Whether PostgreSQL's optional {@code WITH} precedes the options. + * + * @return {@code true} if {@code WITH} is rendered + */ + public boolean isUseWith() { + return useWith; + } + + public void setUseWith(boolean useWith) { + this.useWith = useWith; + } + + /** + * The options of {@link Action#OPTIONS}, as a mutable list. + * + * @return the options in source order + */ + public List getOptions() { + return options; + } + + public Optional getOption(DatabaseOption.Kind kind) { + return options.stream().filter(option -> option.getKind() == kind).findFirst(); + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + /** + * The new owner, a role name or {@code CURRENT_USER}, {@code CURRENT_ROLE} or + * {@code SESSION_USER}. + * + * @return the owner + */ + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getTablespace() { + return tablespace; + } + + public void setTablespace(String tablespace) { + this.tablespace = tablespace; + } + + public ConfigurationParameterChange getParameterChange() { + return parameterChange; + } + + public void setParameterChange(ConfigurationParameterChange parameterChange) { + this.parameterChange = parameterChange; + } + + public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append); + } + + /** + * Appends this statement to a builder. + * + * @param builder the target + * @param valueRenderer appends each configuration parameter value + * @return the builder + */ + public StringBuilder appendTo(StringBuilder builder, Consumer valueRenderer) { + builder.append(useSchemaKeyword ? "ALTER SCHEMA" : "ALTER DATABASE"); + if (databaseName != null) { + builder.append(' ').append(databaseName); + } + switch (action) { + case RENAME: + return builder.append(" RENAME TO ").append(newName); + case OWNER: + return builder.append(" OWNER TO ").append(owner); + case SET_TABLESPACE: + return builder.append(" SET TABLESPACE ").append(tablespace); + case REFRESH_COLLATION_VERSION: + return builder.append(" REFRESH COLLATION VERSION"); + case CONFIGURATION_PARAMETER: + if (parameterChange == null) { + throw new IllegalStateException("Expected a configuration parameter change"); + } + return parameterChange.appendTo(builder.append(' '), valueRenderer); + default: + if (useWith) { + builder.append(" WITH"); + } + options.forEach(option -> builder.append(' ').append(option)); + return builder; + } + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/schema/AlterSchema.java b/src/main/java/net/sf/jsqlparser/statement/alter/schema/AlterSchema.java new file mode 100644 index 0000000000..f88aa08b82 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/schema/AlterSchema.java @@ -0,0 +1,80 @@ +/*- + * #%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.statement.alter.schema; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** PostgreSQL schema name or ownership change. Names retain SQL identifier quoting. */ +public class AlterSchema extends ASTNodeAccessImpl implements Statement { + public enum Action { + RENAME, OWNER + } + + private String schemaName; + private Action action; + private String newName; + private String owner; + + public String getSchemaName() { + return schemaName; + } + + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER SCHEMA ").append(schemaName); + if (action == Action.RENAME) { + builder.append(" RENAME TO ").append(newName); + } else if (action == Action.OWNER) { + builder.append(" OWNER TO ").append(owner); + } else { + throw new IllegalStateException("Expected a schema alteration action"); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/sequence/AlterSequence.java b/src/main/java/net/sf/jsqlparser/statement/alter/sequence/AlterSequence.java index a9869f8ed6..644a8382b1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/sequence/AlterSequence.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/sequence/AlterSequence.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement.alter.sequence; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Sequence; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; @@ -16,9 +17,81 @@ /** * An {@code ALTER SEQUENCE} statement */ -public class AlterSequence implements Statement { +public class AlterSequence extends ASTNodeAccessImpl implements Statement { + + public enum Action { + PARAMETERS, RENAME, OWNER, SET_SCHEMA, SET_LOGGED, SET_UNLOGGED + } public Sequence sequence; + private boolean ifExists; + private Action action = Action.PARAMETERS; + private String newName; + private String owner; + private String schemaName; + + public boolean isIfExists() { + return ifExists; + } + + public void setIfExists(boolean ifExists) { + this.ifExists = ifExists; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = java.util.Objects.requireNonNull(action, "action"); + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getSchemaName() { + return schemaName; + } + + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER SEQUENCE "); + if (ifExists) { + builder.append("IF EXISTS "); + } + builder.append(action == Action.PARAMETERS ? sequence : sequence.getFullyQualifiedName()); + switch (action) { + case RENAME: + return builder.append(" RENAME TO ").append(newName); + case OWNER: + return builder.append(" OWNER TO ").append(owner); + case SET_SCHEMA: + return builder.append(" SET SCHEMA ").append(schemaName); + case SET_LOGGED: + return builder.append(" SET LOGGED"); + case SET_UNLOGGED: + return builder.append(" SET UNLOGGED"); + default: + return builder; + } + } + public Sequence getSequence() { return sequence; @@ -35,9 +108,7 @@ public T accept(StatementVisitor statementVisitor, S context) { @Override public String toString() { - String sql; - sql = "ALTER SEQUENCE " + sequence; - return sql; + return appendTo(new StringBuilder()).toString(); } public AlterSequence withSequence(Sequence sequence) { diff --git a/src/main/java/net/sf/jsqlparser/statement/analyze/Analyze.java b/src/main/java/net/sf/jsqlparser/statement/analyze/Analyze.java index ab25cf3b40..d8d0e01397 100644 --- a/src/main/java/net/sf/jsqlparser/statement/analyze/Analyze.java +++ b/src/main/java/net/sf/jsqlparser/statement/analyze/Analyze.java @@ -9,13 +9,41 @@ */ package net.sf.jsqlparser.statement.analyze; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; -public class Analyze implements Statement { +public class Analyze extends ASTNodeAccessImpl implements Statement { - private Table table; + public enum Modifier { + NO_WRITE_TO_BINLOG, LOCAL + } + + public enum HistogramAction { + UPDATE, DROP + } + + /** Whether MySQL updates a histogram automatically when it updates the table statistics. */ + public enum HistogramUpdateMode { + AUTO, MANUAL + } + + private final List
tables = new ArrayList<>(); + private boolean tableKeyword; + private Modifier modifier; + private HistogramAction histogramAction; + private final List histogramColumns = new ArrayList<>(); + private Integer histogramBucketCount; + private StringValue histogramData; + private HistogramUpdateMode histogramUpdateMode; @Override public T accept(StatementVisitor statementVisitor, S context) { @@ -23,20 +51,168 @@ public T accept(StatementVisitor statementVisitor, S context) { } public Table getTable() { - return table; + return tables.isEmpty() ? null : tables.get(0); } public void setTable(Table table) { - this.table = table; + tables.clear(); + if (table != null) { + tables.add(table); + } + } + + public List
getTables() { + return Collections.unmodifiableList(tables); + } + + public void setTables(Collection tables) { + this.tables.clear(); + if (tables != null) { + this.tables.addAll(tables); + } + } + + public boolean isTableKeyword() { + return tableKeyword; + } + + public void setTableKeyword(boolean tableKeyword) { + this.tableKeyword = tableKeyword; + } + + public Modifier getModifier() { + return modifier; + } + + public void setModifier(Modifier modifier) { + this.modifier = modifier; + } + + public HistogramAction getHistogramAction() { + return histogramAction; + } + + public void setHistogramAction(HistogramAction histogramAction) { + this.histogramAction = histogramAction; + } + + public List getHistogramColumns() { + return Collections.unmodifiableList(histogramColumns); + } + + public void setHistogramColumns(Collection columns) { + histogramColumns.clear(); + if (columns != null) { + histogramColumns.addAll(columns); + } + } + + public Integer getHistogramBucketCount() { + return histogramBucketCount; + } + + public void setHistogramBucketCount(Integer histogramBucketCount) { + this.histogramBucketCount = histogramBucketCount; + } + + /** The JSON histogram of {@code UPDATE HISTOGRAM ... USING DATA}, or null. */ + public StringValue getHistogramData() { + return histogramData; + } + + public void setHistogramData(StringValue histogramData) { + this.histogramData = histogramData; + } + + /** The {@code AUTO UPDATE} or {@code MANUAL UPDATE} of {@code UPDATE HISTOGRAM}, or null. */ + public HistogramUpdateMode getHistogramUpdateMode() { + return histogramUpdateMode; + } + + public void setHistogramUpdateMode(HistogramUpdateMode histogramUpdateMode) { + this.histogramUpdateMode = histogramUpdateMode; } @Override public String toString() { - return "ANALYZE " + table.toString(); + StringBuilder builder = new StringBuilder("ANALYZE"); + if (modifier != null) { + builder.append(" ").append(modifier); + } + if (tableKeyword) { + builder.append(" TABLE"); + } + builder.append(" ").append(tables.stream().map(Table::toString) + .collect(Collectors.joining(", "))); + if (histogramAction != null) { + builder.append(" ").append(histogramAction).append(" HISTOGRAM ON ") + .append(histogramColumns.stream().map(Column::toString) + .collect(Collectors.joining(", "))); + if (histogramBucketCount != null) { + builder.append(" WITH ").append(histogramBucketCount).append(" BUCKETS"); + } + if (histogramData != null) { + builder.append(" USING DATA ").append(histogramData); + } + if (histogramUpdateMode != null) { + builder.append(" ").append(histogramUpdateMode).append(" UPDATE"); + } + } + return builder.toString(); } public Analyze withTable(Table table) { this.setTable(table); return this; } + + public Analyze withTables(Collection tables) { + setTables(tables); + return this; + } + + public Analyze addTables(Table... tables) { + Collections.addAll(this.tables, tables); + return this; + } + + public Analyze withTableKeyword(boolean tableKeyword) { + setTableKeyword(tableKeyword); + return this; + } + + public Analyze withModifier(Modifier modifier) { + setModifier(modifier); + return this; + } + + public Analyze withHistogramAction(HistogramAction histogramAction) { + setHistogramAction(histogramAction); + return this; + } + + public Analyze withHistogramColumns(Collection columns) { + setHistogramColumns(columns); + return this; + } + + public Analyze addHistogramColumns(Column... columns) { + Collections.addAll(histogramColumns, columns); + return this; + } + + public Analyze withHistogramBucketCount(Integer histogramBucketCount) { + setHistogramBucketCount(histogramBucketCount); + return this; + } + + public Analyze withHistogramData(StringValue histogramData) { + setHistogramData(histogramData); + return this; + } + + public Analyze withHistogramUpdateMode(HistogramUpdateMode histogramUpdateMode) { + setHistogramUpdateMode(histogramUpdateMode); + return this; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/comment/Comment.java b/src/main/java/net/sf/jsqlparser/statement/comment/Comment.java index d213aac6b8..f5f95ac245 100755 --- a/src/main/java/net/sf/jsqlparser/statement/comment/Comment.java +++ b/src/main/java/net/sf/jsqlparser/statement/comment/Comment.java @@ -9,17 +9,20 @@ */ package net.sf.jsqlparser.statement.comment; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; -public class Comment implements Statement { +public class Comment extends ASTNodeAccessImpl implements Statement { private Table table; private Column column; private Table view; + private CommentTarget target; private StringValue comment; @Override @@ -33,6 +36,9 @@ public Table getTable() { public void setTable(Table table) { this.table = table; + if (table != null) { + target = null; + } } public Column getColumn() { @@ -41,6 +47,9 @@ public Column getColumn() { public void setColumn(Column column) { this.column = column; + if (column != null) { + target = null; + } } public Table getView() { @@ -49,6 +58,39 @@ public Table getView() { public void setView(Table view) { this.view = view; + if (view != null) { + target = null; + } + } + + /** Additional catalog targets; the existing table, column and view accessors remain intact. */ + public CommentTarget getTarget() { + return target; + } + + public void setTarget(CommentTarget target) { + this.target = target; + if (target != null) { + table = null; + column = null; + view = null; + } + } + + public Comment withTarget(CommentTarget target) { + setTarget(target); + return this; + } + + /** Visits the relation explicitly named by this comment, without resolving catalog objects. */ + public void visitRelations(Consumer
visitor) { + Table relation = table != null ? table + : column != null ? column.getTable() + : view != null ? view + : target != null ? target.getReferencedRelation() : null; + if (relation != null) { + visitor.accept(relation); + } } public StringValue getComment() { @@ -61,16 +103,37 @@ public void setComment(StringValue comment) { @Override public String toString() { - String sql = "COMMENT ON "; + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append, builder::append, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer
relationWriter, + Consumer columnWriter, Consumer commentWriter) { + builder.append("COMMENT ON "); if (table != null) { - sql += "TABLE " + table + " "; + builder.append("TABLE "); + relationWriter.accept(table); + builder.append(' '); } else if (column != null) { - sql += "COLUMN " + column + " "; + builder.append("COLUMN "); + columnWriter.accept(column); + builder.append(' '); } else if (view != null) { - sql += "VIEW " + view + " "; + builder.append("VIEW "); + relationWriter.accept(view); + builder.append(' '); + } else if (target != null) { + target.appendTo(builder, relationWriter); + builder.append(' '); + } + // a null comment stands for PostgreSQL's COMMENT ON ... IS NULL, which removes the comment + builder.append("IS "); + if (comment == null) { + builder.append("NULL"); + } else { + commentWriter.accept(comment); } - sql += "IS " + comment; - return sql; + return builder; } public Comment withTable(Table table) { diff --git a/src/main/java/net/sf/jsqlparser/statement/comment/CommentTarget.java b/src/main/java/net/sf/jsqlparser/statement/comment/CommentTarget.java new file mode 100644 index 0000000000..5cd3e0009d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/comment/CommentTarget.java @@ -0,0 +1,276 @@ +/*- + * #%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.statement.comment; + +import java.io.Serializable; +import java.util.function.Consumer; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ExclusionOperator; + +/** A catalog object addressed by COMMENT, rather than a function invocation or a query. */ +public class CommentTarget implements Serializable { + public enum Kind { + INDEX, SCHEMA, SEQUENCE, DOMAIN, TYPE, MATERIALIZED_VIEW, FUNCTION, CONSTRAINT, + + TRIGGER, POLICY, RULE, PROCEDURE, ROUTINE, AGGREGATE, CAST, + + COLLATION, FOREIGN_TABLE, STATISTICS, DATABASE, ROLE, TABLESPACE, ACCESS_METHOD, EXTENSION, + + FOREIGN_DATA_WRAPPER, LANGUAGE, SERVER, EVENT_TRIGGER, PUBLICATION, SUBSCRIPTION, + + OPERATOR, OPERATOR_CLASS, OPERATOR_FAMILY, LARGE_OBJECT, CONVERSION, TRANSFORM, + + TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE; + + /** + * Whether the target is a routine signature, exposed by {@link CommentTarget#getRoutine()}. + */ + public boolean isRoutine() { + return this == FUNCTION || this == PROCEDURE || this == ROUTINE || this == AGGREGATE; + } + + /** + * Whether the target is named within the table or domain of + * {@link CommentTarget#getRelation()}. + */ + public boolean isOwnedByRelation() { + return this == CONSTRAINT || this == TRIGGER || this == POLICY || this == RULE; + } + + /** + * Whether the target is named within the index access method of + * {@link CommentTarget#getAccessMethod()}. + */ + public boolean isOwnedByAccessMethod() { + return this == OPERATOR_CLASS || this == OPERATOR_FAMILY; + } + } + + private Kind kind; + private Table name; + private RoutineReference routine; + private Table relation; + private boolean onDomain; + private boolean procedural; + private ColDataType sourceType; + private ColDataType targetType; + private ExclusionOperator operator; + private ColDataType leftOperandType; + private ColDataType rightOperandType; + private String accessMethod; + private Long largeObjectOid; + private ColDataType transformType; + private String language; + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + /** + * The object's identifier; using Table preserves the individual name components. Routine, cast, + * operator, large object and transform targets have no name. + */ + public Table getName() { + return name; + } + + public void setName(Table name) { + this.name = name; + } + + public RoutineReference getRoutine() { + return routine; + } + + public void setRoutine(RoutineReference routine) { + this.routine = routine; + } + + /** + * The table owning a constraint, trigger, policy or rule, or the domain owning a constraint, + * distinguished by {@link #isOnDomain()}. + */ + public Table getRelation() { + return relation; + } + + public void setRelation(Table relation) { + this.relation = relation; + } + + public boolean isOnDomain() { + return onDomain; + } + + public void setOnDomain(boolean onDomain) { + this.onDomain = onDomain; + } + + /** Whether a {@link Kind#LANGUAGE} target was written as PROCEDURAL LANGUAGE. */ + public boolean isProcedural() { + return procedural; + } + + public void setProcedural(boolean procedural) { + this.procedural = procedural; + } + + /** The source type of a {@link Kind#CAST} target. */ + public ColDataType getSourceType() { + return sourceType; + } + + public void setSourceType(ColDataType sourceType) { + this.sourceType = sourceType; + } + + /** The target type of a {@link Kind#CAST} target. */ + public ColDataType getTargetType() { + return targetType; + } + + public void setTargetType(ColDataType targetType) { + this.targetType = targetType; + } + + /** + * The possibly schema-qualified operator of an {@link Kind#OPERATOR} target, read by the + * operator reference shared with exclusion constraints and ALTER EXTENSION. + */ + public ExclusionOperator getOperator() { + return operator; + } + + public void setOperator(ExclusionOperator operator) { + this.operator = operator; + } + + /** The left operand type of an {@link Kind#OPERATOR} target; null stands for NONE. */ + public ColDataType getLeftOperandType() { + return leftOperandType; + } + + public void setLeftOperandType(ColDataType leftOperandType) { + this.leftOperandType = leftOperandType; + } + + /** The right operand type of an {@link Kind#OPERATOR} target; null stands for NONE. */ + public ColDataType getRightOperandType() { + return rightOperandType; + } + + public void setRightOperandType(ColDataType rightOperandType) { + this.rightOperandType = rightOperandType; + } + + /** + * The index access method of an {@link Kind#OPERATOR_CLASS} or {@link Kind#OPERATOR_FAMILY} + * target. + */ + public String getAccessMethod() { + return accessMethod; + } + + public void setAccessMethod(String accessMethod) { + this.accessMethod = accessMethod; + } + + /** The OID of a {@link Kind#LARGE_OBJECT} target. */ + public Long getLargeObjectOid() { + return largeObjectOid; + } + + public void setLargeObjectOid(Long largeObjectOid) { + this.largeObjectOid = largeObjectOid; + } + + /** The data type of a {@link Kind#TRANSFORM} target. */ + public ColDataType getTransformType() { + return transformType; + } + + public void setTransformType(ColDataType transformType) { + this.transformType = transformType; + } + + /** The procedural language of a {@link Kind#TRANSFORM} target. */ + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + /** + * Returns only an explicitly named table, foreign table or view, never an index, type, + * function, statistics object or domain. + */ + public Table getReferencedRelation() { + if (namesRelation()) { + return name; + } + return kind != null && kind.isOwnedByRelation() && !onDomain ? relation : null; + } + + private boolean namesRelation() { + return kind == Kind.MATERIALIZED_VIEW || kind == Kind.FOREIGN_TABLE; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer
relationWriter) { + if (kind == Kind.LANGUAGE && procedural) { + builder.append("PROCEDURAL "); + } + builder.append(kind.name().replace('_', ' ')).append(' '); + if (kind.isRoutine()) { + builder.append(routine); + } else if (kind == Kind.CAST) { + builder.append('(').append(sourceType).append(" AS ").append(targetType).append(')'); + } else if (kind == Kind.OPERATOR) { + builder.append(operator).append(" (").append(operandType(leftOperandType)) + .append(", ").append(operandType(rightOperandType)).append(')'); + } else if (kind == Kind.LARGE_OBJECT) { + builder.append(largeObjectOid); + } else if (kind == Kind.TRANSFORM) { + builder.append("FOR ").append(transformType).append(" LANGUAGE ").append(language); + } else if (namesRelation()) { + relationWriter.accept(name); + } else { + builder.append(name); + if (kind.isOwnedByRelation()) { + builder.append(" ON "); + if (onDomain) { + builder.append("DOMAIN ").append(relation); + } else { + relationWriter.accept(relation); + } + } else if (kind.isOwnedByAccessMethod()) { + builder.append(" USING ").append(accessMethod); + } + } + return builder; + } + + private static Object operandType(ColDataType type) { + return type == null ? "NONE" : type; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/accessmethod/CreateAccessMethod.java b/src/main/java/net/sf/jsqlparser/statement/create/accessmethod/CreateAccessMethod.java new file mode 100644 index 0000000000..feb0086435 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/accessmethod/CreateAccessMethod.java @@ -0,0 +1,64 @@ +/*- + * #%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.statement.create.accessmethod; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** A PostgreSQL table or index access method, whose handler names a function, not a table. */ +public class CreateAccessMethod extends ASTNodeAccessImpl implements Statement { + public enum Type { + TABLE, INDEX + } + + private String name; + private Type type; + private String handler; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public String getHandler() { + return handler; + } + + public void setHandler(String handler) { + this.handler = handler; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + public StringBuilder appendTo(StringBuilder builder) { + return builder.append("CREATE ACCESS METHOD ").append(name) + .append(" TYPE ").append(type).append(" HANDLER ").append(handler); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java b/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java new file mode 100644 index 0000000000..9aa9dcc732 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java @@ -0,0 +1,97 @@ +/*- + * #%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.statement.create.collation; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.IndexOption; + +/** PostgreSQL collation copy or parameter definition, with mutually exclusive forms. */ +public class CreateCollation extends ASTNodeAccessImpl implements Statement { + private String name; + private boolean ifNotExists; + private String sourceCollation; + private List options; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean value) { + ifNotExists = value; + } + + public String getSourceCollation() { + return sourceCollation; + } + + public void setSourceCollation(String source) { + sourceCollation = source; + if (source != null) { + options = null; + } + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + if (options != null) { + sourceCollation = null; + } + } + + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("CREATE COLLATION "); + if (isIfNotExists()) { + sql.append("IF NOT EXISTS "); + } + sql.append(getName()); + if (sourceCollation != null) { + sql.append(" FROM ").append(sourceCollation); + } else { + sql.append(' '); + IndexOption.appendListTo(sql, options, printer); + } + return sql; + } + + public void visitExpressions(Consumer visitor) { + if (sourceCollation == null && options != null) { + options.stream().map(IndexOption::getValue).filter(java.util.Objects::nonNull) + .forEach(visitor); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java b/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java new file mode 100644 index 0000000000..4376ce0757 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java @@ -0,0 +1,207 @@ +/*- + * #%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.statement.create.database; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * A {@code CREATE DATABASE} statement with an optional {@code IF NOT EXISTS} and options. MySQL and + * PostgreSQL options are typed {@link DatabaseOption}s; other option text is kept as a raw + * {@link DatabaseOption.Kind#OTHER} option. + */ +public class CreateDatabase extends ASTNodeAccessImpl implements Statement { + + private String databaseName; + private boolean hasIfNotExists = false; + private boolean useWith; + private final List options = new ArrayList<>(); + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + /** + * The name of the database. + * + * @return the database name + */ + public String getDatabaseName() { + return databaseName; + } + + public CreateDatabase setDatabaseName(String databaseName) { + this.databaseName = databaseName; + return this; + } + + public boolean hasIfNotExists() { + return hasIfNotExists; + } + + public CreateDatabase setIfNotExists(boolean hasIfNotExists) { + this.hasIfNotExists = hasIfNotExists; + return this; + } + + /** + * Whether PostgreSQL's optional {@code WITH} precedes the options. + * + * @return {@code true} if {@code WITH} is rendered + */ + public boolean isUseWith() { + return useWith; + } + + public CreateDatabase setUseWith(boolean useWith) { + this.useWith = useWith; + return this; + } + + /** + * The options in their legacy flat-token form, including a leading {@code WITH}. This is an + * unmodifiable snapshot of {@link #getOptions()}; edit the options or use + * {@link #setDatabaseOptions(List)} instead. + * + * @return the option tokens, {@code null} when no options were given + */ + public List getDatabaseOptions() { + if (options.isEmpty() && !useWith) { + return null; + } + List tokens = new ArrayList<>(); + if (useWith) { + tokens.add("WITH"); + } + options.forEach(option -> tokens.addAll(option.getTokens())); + return Collections.unmodifiableList(tokens); + } + + /** + * Replaces all options, including {@code WITH}, with one raw option of these tokens. The tokens + * are not parsed. + * + * @param databaseOptions the option tokens, or {@code null} to remove all options + * @return this statement + */ + public CreateDatabase setDatabaseOptions(List databaseOptions) { + options.clear(); + useWith = false; + if (databaseOptions != null && !databaseOptions.isEmpty()) { + options.add(DatabaseOption.raw(databaseOptions)); + } + return this; + } + + /** + * The options in source order, as a mutable list. + * + * @return the options, empty if there are none + */ + public List getOptions() { + return options; + } + + /** + * Replaces the options with a copy of the given list. + * + * @param options the options, or {@code null} to remove all options + * @return this statement + */ + public CreateDatabase setOptions(List options) { + List copy = + options == null ? Collections.emptyList() : new ArrayList<>(options); + this.options.clear(); + this.options.addAll(copy); + return this; + } + + public Optional getOption(DatabaseOption.Kind kind) { + return options.stream().filter(option -> option.getKind() == kind).findFirst(); + } + + public CreateDatabase withDatabaseName(String databaseName) { + return this.setDatabaseName(databaseName); + } + + public CreateDatabase withIfNotExists(boolean hasIfNotExists) { + return this.setIfNotExists(hasIfNotExists); + } + + public CreateDatabase withUseWith(boolean useWith) { + return this.setUseWith(useWith); + } + + public CreateDatabase withDatabaseOptions(List databaseOptions) { + return this.setDatabaseOptions(databaseOptions); + } + + public CreateDatabase withOptions(List options) { + return setOptions(options); + } + + public CreateDatabase addOptions(DatabaseOption... options) { + Collections.addAll(this.options, options); + return this; + } + + /** + * Appends one raw option of these tokens, keeping the existing options. + * + * @param databaseOptions the option tokens + * @return this statement + */ + public CreateDatabase addDatabaseOptions(String... databaseOptions) { + return addDatabaseOptions(Arrays.asList(databaseOptions)); + } + + /** + * Appends one raw option of these tokens, keeping the existing options. + * + * @param databaseOptions the option tokens + * @return this statement + */ + public CreateDatabase addDatabaseOptions(Collection databaseOptions) { + if (!databaseOptions.isEmpty()) { + options.add(DatabaseOption.raw(new ArrayList<>(databaseOptions))); + } + return this; + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder("CREATE DATABASE"); + if (hasIfNotExists) { + sql.append(" IF NOT EXISTS"); + } + if (databaseName != null) { + sql.append(' ').append(databaseName); + } + if (useWith) { + sql.append(" WITH"); + } + for (DatabaseOption option : options) { + String text = option.toString(); + if (!text.isEmpty()) { + sql.append(' ').append(text); + } + } + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/database/DatabaseOption.java b/src/main/java/net/sf/jsqlparser/statement/create/database/DatabaseOption.java new file mode 100644 index 0000000000..4a6c2b7615 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/database/DatabaseOption.java @@ -0,0 +1,152 @@ +/*- + * #%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.statement.create.database; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A structured option shared by MySQL {@code CREATE/ALTER DATABASE} and + * {@code CREATE/ALTER SCHEMA}, and by PostgreSQL {@code CREATE/ALTER DATABASE}. The name keeps the + * written spelling, for example {@code CHARSET} for {@link Kind#CHARACTER_SET}. Values keep their + * SQL text, including string quotes and signs. Unrecognized option text is an {@link Kind#OTHER} + * option with raw tokens. + */ +public class DatabaseOption implements Serializable { + + /** + * The option. MySQL uses {@code CHARACTER_SET} to {@code READ_ONLY}, PostgreSQL uses + * {@code OWNER} to {@code OID}, and {@code OTHER} holds raw tokens. + */ + public enum Kind { + CHARACTER_SET, COLLATE, ENCRYPTION, READ_ONLY, OWNER, TEMPLATE, ENCODING, STRATEGY, LOCALE, LC_COLLATE, LC_CTYPE, BUILTIN_LOCALE, ICU_LOCALE, ICU_RULES, LOCALE_PROVIDER, COLLATION_VERSION, TABLESPACE, ALLOW_CONNECTIONS, CONNECTION_LIMIT, IS_TEMPLATE, OID, OTHER + } + + private Kind kind = Kind.OTHER; + private String name; + private String value; + private boolean useDefault; + private boolean useEquals; + private List rawTokens; + + public DatabaseOption() {} + + public DatabaseOption(Kind kind, String name, String value, boolean useDefault, + boolean useEquals) { + this.kind = kind; + this.name = name; + this.value = value; + this.useDefault = useDefault; + this.useEquals = useEquals; + } + + public static DatabaseOption raw(List tokens) { + DatabaseOption option = new DatabaseOption(); + option.setRawTokens(tokens); + return option; + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public boolean isUseDefault() { + return useDefault; + } + + public void setUseDefault(boolean useDefault) { + this.useDefault = useDefault; + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + public List getRawTokens() { + return rawTokens; + } + + public void setRawTokens(List rawTokens) { + this.rawTokens = rawTokens; + } + + /** Returns this option in the legacy flat-token representation. */ + public List getTokens() { + if (rawTokens != null) { + return Collections.unmodifiableList(rawTokens); + } + List tokens = new ArrayList<>(); + if (useDefault) { + tokens.add("DEFAULT"); + } + Collections.addAll(tokens, name.split(" ")); + if (useEquals) { + tokens.add("="); + } + tokens.add(value); + return tokens; + } + + @Override + public String toString() { + return String.join(" ", getTokens()); + } + + public DatabaseOption withKind(Kind kind) { + setKind(kind); + return this; + } + + public DatabaseOption withName(String name) { + setName(name); + return this; + } + + public DatabaseOption withValue(String value) { + setValue(value); + return this; + } + + public DatabaseOption withUseDefault(boolean useDefault) { + setUseDefault(useDefault); + return this; + } + + public DatabaseOption withUseEquals(boolean useEquals) { + setUseEquals(useEquals); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/domain/CreateDomain.java b/src/main/java/net/sf/jsqlparser/statement/create/domain/CreateDomain.java new file mode 100644 index 0000000000..f079ca7b7d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/domain/CreateDomain.java @@ -0,0 +1,103 @@ +/*- + * #%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.statement.create.domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class CreateDomain extends ASTNodeAccessImpl implements Statement { + private String name; + private boolean useAs; + private ColDataType dataType; + private String collation; + private Expression defaultExpression; + private List constraints = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isUseAs() { + return useAs; + } + + public void setUseAs(boolean useAs) { + this.useAs = useAs; + } + + public ColDataType getDataType() { + return dataType; + } + + public void setDataType(ColDataType dataType) { + this.dataType = dataType; + } + + public String getCollation() { + return collation; + } + + public void setCollation(String collation) { + this.collation = collation; + } + + public Expression getDefaultExpression() { + return defaultExpression; + } + + public void setDefaultExpression(Expression defaultExpression) { + this.defaultExpression = defaultExpression; + } + + public List getConstraints() { + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + public void appendTo(StringBuilder sql, Consumer expressions) { + sql.append("CREATE DOMAIN ").append(name).append(useAs ? " AS " : " ").append(dataType); + if (collation != null) { + sql.append(" COLLATE ").append(collation); + } + if (defaultExpression != null) { + sql.append(" DEFAULT "); + expressions.accept(defaultExpression); + } + for (DomainConstraint constraint : constraints) { + sql.append(' '); + constraint.appendTo(sql, expressions); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/domain/DomainConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/domain/DomainConstraint.java new file mode 100644 index 0000000000..7b42733d72 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/domain/DomainConstraint.java @@ -0,0 +1,68 @@ +/*- + * #%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.statement.create.domain; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import java.io.Serializable; + +public class DomainConstraint implements Serializable { + private String name; + private Kind kind; + private Expression expression; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public enum Kind { + NULL, NOT_NULL, CHECK + } + + public void appendTo(StringBuilder sql, Consumer expressions) { + if (name != null) { + sql.append("CONSTRAINT ").append(name).append(' '); + } + if (kind == Kind.CHECK) { + sql.append("CHECK ("); + expressions.accept(expression); + sql.append(')'); + } else { + sql.append(kind == Kind.NULL ? "NULL" : "NOT NULL"); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/event/AlterEvent.java b/src/main/java/net/sf/jsqlparser/statement/create/event/AlterEvent.java new file mode 100644 index 0000000000..52972cf7ed --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/event/AlterEvent.java @@ -0,0 +1,53 @@ +/*- + * #%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.statement.create.event; + +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** MySQL {@code ALTER EVENT} statement. */ +public class AlterEvent extends EventStatement { + + private Table renameTo; + + public Table getRenameTo() { + return renameTo; + } + + public void setRenameTo(Table renameTo) { + this.renameTo = renameTo; + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("ALTER "); + appendDefinerAndEvent(builder); + builder.append(getEvent()); + appendScheduleAndCompletion(builder); + if (renameTo != null) { + builder.append(" RENAME TO ").append(renameTo); + } + appendStatusAndComment(builder); + if (getBody() != null) { + builder.append(" DO ").append(getBody()); + } + return builder.toString(); + } + + public AlterEvent withRenameTo(Table renameTo) { + setRenameTo(renameTo); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/event/CreateEvent.java b/src/main/java/net/sf/jsqlparser/statement/create/event/CreateEvent.java new file mode 100644 index 0000000000..6b355f49d1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/event/CreateEvent.java @@ -0,0 +1,50 @@ +/*- + * #%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.statement.create.event; + +import net.sf.jsqlparser.statement.StatementVisitor; + +/** MySQL {@code CREATE EVENT} statement. */ +public class CreateEvent extends EventStatement { + + private boolean ifNotExists; + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("CREATE "); + appendDefinerAndEvent(builder); + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + builder.append(getEvent()); + appendScheduleAndCompletion(builder); + appendStatusAndComment(builder); + builder.append(" DO ").append(getBody()); + return builder.toString(); + } + + public CreateEvent withIfNotExists(boolean ifNotExists) { + setIfNotExists(ifNotExists); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/event/EventSchedule.java b/src/main/java/net/sf/jsqlparser/statement/create/event/EventSchedule.java new file mode 100644 index 0000000000..b3dba66f19 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/event/EventSchedule.java @@ -0,0 +1,123 @@ +/*- + * #%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.statement.create.event; + +import java.io.Serializable; +import net.sf.jsqlparser.expression.Expression; + +/** Structured {@code ON SCHEDULE} clause of a MySQL event. */ +public class EventSchedule implements Serializable { + + public enum Type { + AT, EVERY + } + + private Type type; + private Expression executeAt; + private Expression interval; + private String intervalUnit; + private Expression starts; + private Expression ends; + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public Expression getExecuteAt() { + return executeAt; + } + + public void setExecuteAt(Expression executeAt) { + this.executeAt = executeAt; + } + + public Expression getInterval() { + return interval; + } + + public void setInterval(Expression interval) { + this.interval = interval; + } + + public String getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(String intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Expression getStarts() { + return starts; + } + + public void setStarts(Expression starts) { + this.starts = starts; + } + + public Expression getEnds() { + return ends; + } + + public void setEnds(Expression ends) { + this.ends = ends; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (type == Type.AT) { + builder.append("AT ").append(executeAt); + } else if (type == Type.EVERY) { + builder.append("EVERY ").append(interval).append(" ").append(intervalUnit); + if (starts != null) { + builder.append(" STARTS ").append(starts); + } + if (ends != null) { + builder.append(" ENDS ").append(ends); + } + } + return builder.toString(); + } + + public EventSchedule withType(Type type) { + setType(type); + return this; + } + + public EventSchedule withExecuteAt(Expression executeAt) { + setExecuteAt(executeAt); + return this; + } + + public EventSchedule withInterval(Expression interval) { + setInterval(interval); + return this; + } + + public EventSchedule withIntervalUnit(String intervalUnit) { + setIntervalUnit(intervalUnit); + return this; + } + + public EventSchedule withStarts(Expression starts) { + setStarts(starts); + return this; + } + + public EventSchedule withEnds(Expression ends) { + setEnds(ends); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatement.java b/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatement.java new file mode 100644 index 0000000000..409fc67237 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatement.java @@ -0,0 +1,118 @@ +/*- + * #%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.statement.create.event; + +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.MySqlDefiner; +import net.sf.jsqlparser.statement.Statement; + +/** Common structured properties of MySQL {@code CREATE EVENT} and {@code ALTER EVENT}. */ +public abstract class EventStatement extends ASTNodeAccessImpl implements Statement { + + private MySqlDefiner definer; + private Table event; + private EventSchedule schedule; + private Boolean onCompletionPreserve; + private EventStatus status; + private StringValue comment; + private Statement body; + + public MySqlDefiner getDefiner() { + return definer; + } + + public void setDefiner(MySqlDefiner definer) { + this.definer = definer; + } + + public Table getEvent() { + return event; + } + + public void setEvent(Table event) { + this.event = event; + } + + public EventSchedule getSchedule() { + return schedule; + } + + public void setSchedule(EventSchedule schedule) { + this.schedule = schedule; + } + + /** + * Returns {@code true} for {@code ON COMPLETION PRESERVE}, {@code false} for + * {@code ON COMPLETION NOT PRESERVE}, or {@code null} when omitted. + */ + public Boolean getOnCompletionPreserve() { + return onCompletionPreserve; + } + + public void setOnCompletionPreserve(Boolean onCompletionPreserve) { + this.onCompletionPreserve = onCompletionPreserve; + } + + public EventStatus getStatus() { + return status; + } + + public void setStatus(EventStatus status) { + this.status = status; + } + + public StringValue getComment() { + return comment; + } + + public void setComment(StringValue comment) { + this.comment = comment; + } + + public Statement getBody() { + return body; + } + + public void setBody(Statement body) { + this.body = body; + } + + /** Appends the optional DEFINER clause, followed by EVENT, after CREATE or ALTER. */ + protected void appendDefinerAndEvent(StringBuilder builder) { + if (definer != null) { + builder.append("DEFINER = ").append(definer).append(' '); + } + builder.append("EVENT "); + } + + protected void appendScheduleAndCompletion(StringBuilder builder) { + if (schedule != null) { + builder.append(" ON SCHEDULE ").append(schedule); + } + if (onCompletionPreserve != null) { + builder.append(" ON COMPLETION "); + if (!onCompletionPreserve) { + builder.append("NOT "); + } + builder.append("PRESERVE"); + } + } + + protected void appendStatusAndComment(StringBuilder builder) { + if (status != null) { + builder.append(" ").append(status); + } + if (comment != null) { + builder.append(" COMMENT ").append(comment); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatus.java b/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatus.java new file mode 100644 index 0000000000..cec7cd2ef3 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatus.java @@ -0,0 +1,26 @@ +/*- + * #%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.statement.create.event; + +public enum EventStatus { + ENABLE("ENABLE"), DISABLE("DISABLE"), DISABLE_ON_SLAVE("DISABLE ON SLAVE"), DISABLE_ON_REPLICA( + "DISABLE ON REPLICA"); + + private final String sql; + + EventStatus(String sql) { + this.sql = sql; + } + + @Override + public String toString() { + return sql; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/extension/CreateExtension.java b/src/main/java/net/sf/jsqlparser/statement/create/extension/CreateExtension.java new file mode 100644 index 0000000000..ea9b25fe39 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/extension/CreateExtension.java @@ -0,0 +1,105 @@ +/*- + * #%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.statement.create.extension; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class CreateExtension extends ASTNodeAccessImpl implements Statement { + private String name; + private boolean ifNotExists; + private boolean useWith; + private List
tables, Consumer expressions) { + tables.accept(table); + if (columns != null) { + columns.forEach(expressions); + } + if (where != null) { + expressions.accept(where); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/publication/PublicationTarget.java b/src/main/java/net/sf/jsqlparser/statement/create/publication/PublicationTarget.java new file mode 100644 index 0000000000..97205da8c7 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/publication/PublicationTarget.java @@ -0,0 +1,78 @@ +/*- + * #%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.statement.create.publication; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Table; +import java.io.Serializable; + +public class PublicationTarget implements Serializable { + private Kind kind; + private List tables = new ArrayList<>(); + private List schemas = new ArrayList<>(); + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public List getTables() { + return tables; + } + + public void setTables(List tables) { + this.tables = tables; + } + + public List getSchemas() { + return schemas; + } + + public void setSchemas(List schemas) { + this.schemas = schemas; + } + + public enum Kind { + TABLE, TABLES_IN_SCHEMA + } + + public void appendTo(StringBuilder sql, Consumer expressions) { + if (kind == Kind.TABLES_IN_SCHEMA) { + sql.append("TABLES IN SCHEMA ").append(String.join(", ", schemas)); + } else { + sql.append("TABLE "); + for (int i = 0; i < tables.size(); i++) { + if (i > 0) { + sql.append(", "); + } + tables.get(i).appendTo(sql, expressions); + } + } + } + + public void visit(Consumer
tableVisitor, Consumer expressions) { + if (kind == Kind.TABLE) { + tables.forEach(table -> table.visit(tableVisitor, expressions)); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/rule/CreateRule.java b/src/main/java/net/sf/jsqlparser/statement/create/rule/CreateRule.java new file mode 100644 index 0000000000..a9984106cb --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/rule/CreateRule.java @@ -0,0 +1,171 @@ +/*- + * #%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.statement.create.rule; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** PostgreSQL rewrite rule whose actions reuse ordinary statement AST nodes. */ +public class CreateRule extends ASTNodeAccessImpl implements Statement { + public enum Event { + SELECT, INSERT, UPDATE, DELETE + } + public enum Behavior { + ALSO, INSTEAD + } + + private String name; + private boolean orReplace; + private Event event; + private Table table; + private Expression whereExpression; + private Behavior behavior; + private boolean useParentheses; + private List actions = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public boolean isOrReplace() { + return orReplace; + } + + public void setOrReplace(boolean value) { + orReplace = value; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event value) { + event = value; + } + + public Table getTable() { + return table; + } + + public void setTable(Table value) { + table = value; + } + + public Expression getWhereExpression() { + return whereExpression; + } + + public void setWhereExpression(Expression value) { + whereExpression = value; + } + + public Behavior getBehavior() { + return behavior; + } + + public void setBehavior(Behavior value) { + behavior = value; + } + + public boolean isUseParentheses() { + return useParentheses; + } + + public void setUseParentheses(boolean value) { + useParentheses = value; + } + + public List getActions() { + return actions; + } + + public void setActions(List value) { + actions = value; + } + + public boolean isNothing() { + return actions.isEmpty() && !useParentheses; + } + + public void setNothing(boolean nothing) { + if (nothing) { + actions.clear(); + useParentheses = false; + } + } + + public void visitExpressions(Consumer visitor) { + if (whereExpression != null) { + visitor.accept(whereExpression); + } + } + + public void visitTables(Consumer
visitor) { + if (table != null) { + visitor.accept(table); + } + } + + public StringBuilder appendTo(StringBuilder sql, Consumer expressions, + Consumer statements) { + sql.append("CREATE "); + if (orReplace) { + sql.append("OR REPLACE "); + } + sql.append("RULE ").append(name).append(" AS ON ").append(event).append(" TO ") + .append(table); + if (whereExpression != null) { + sql.append(" WHERE "); + expressions.accept(whereExpression); + } + sql.append(" DO "); + if (behavior != null) { + sql.append(behavior).append(' '); + } + if (isNothing()) { + return sql.append("NOTHING"); + } + boolean grouped = useParentheses || actions.size() != 1; + if (grouped) { + sql.append('('); + } + for (int i = 0; i < actions.size(); i++) { + if (i > 0) { + sql.append("; "); + } + statements.accept(actions.get(i)); + } + if (grouped) { + sql.append(')'); + } + return sql; + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append, sql::append).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java b/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java index e972c9c30a..bff382a374 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java @@ -14,17 +14,22 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.database.DatabaseOption; -public class CreateSchema implements Statement { +public class CreateSchema extends ASTNodeAccessImpl implements Statement { private String authorization; private String catalogName = null; private String schemaName; private List schemaPath; private List statements = new ArrayList<>(); + private List databaseOptions; private boolean hasIfNotExists = false; @Override @@ -33,7 +38,7 @@ public T accept(StatementVisitor statementVisitor, S context) { } /** - * Add a statement to the schema definition + * Add a schema element, for example a CREATE TABLE or GRANT statement, to the schema definition * * @param statement The statement to be added * @return true if the operation was successful @@ -106,7 +111,8 @@ public void setSchemaPath(List schemaPath) { } /** - * The statements executed as part of the schema creation + * The schema elements created as part of the schema, in source order. Their names are kept as + * written; unqualified names refer to the new schema. * * @return the statements */ @@ -114,6 +120,19 @@ public List getStatements() { return statements; } + public List getDatabaseOptions() { + return databaseOptions; + } + + public void setDatabaseOptions(List databaseOptions) { + this.databaseOptions = databaseOptions; + } + + public Optional getDatabaseOption(DatabaseOption.Kind kind) { + return Optional.ofNullable(databaseOptions).orElseGet(Collections::emptyList).stream() + .filter(option -> option.getKind() == kind).findFirst(); + } + public boolean hasIfNotExists() { return hasIfNotExists; } @@ -123,7 +142,32 @@ public CreateSchema setIfNotExists(boolean hasIfNotExists) { return this; } + @Override public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + /** + * Appends the schema definition, rendering each schema element with the given writer. + * + * @param builder the target + * @param elementWriter renders one schema element + * @return the target + */ + public StringBuilder appendTo(StringBuilder builder, Consumer elementWriter) { + builder.append(headerSql()); + if (schemaPath != null && !schemaPath.isEmpty()) { + builder.append(" PATH ").append(String.join(", ", schemaPath)); + } + for (Statement statement : statements) { + builder.append(' '); + elementWriter.accept(statement); + } + return builder; + } + + private String headerSql() { String sql = "CREATE SCHEMA"; if (hasIfNotExists) { sql += " IF NOT EXISTS"; @@ -131,11 +175,15 @@ public String toString() { if (schemaName != null) { sql += " "; - if (catalogName!=null) { + if (catalogName != null) { sql += catalogName + "."; } sql += schemaName; } + if (databaseOptions != null && !databaseOptions.isEmpty()) { + sql += " " + databaseOptions.stream().map(Object::toString) + .collect(Collectors.joining(" ")); + } if (authorization != null) { sql += " AUTHORIZATION " + authorization; } @@ -157,6 +205,18 @@ public CreateSchema withSchemaPath(List schemaPath) { return this; } + public CreateSchema withDatabaseOptions(List databaseOptions) { + setDatabaseOptions(databaseOptions); + return this; + } + + public CreateSchema addDatabaseOptions(DatabaseOption... databaseOptions) { + List collection = + Optional.ofNullable(getDatabaseOptions()).orElseGet(ArrayList::new); + Collections.addAll(collection, databaseOptions); + return withDatabaseOptions(collection); + } + public CreateSchema addSchemaPath(String... schemaPath) { List collection = Optional.ofNullable(getSchemaPath()).orElseGet(ArrayList::new); Collections.addAll(collection, schemaPath); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/sequence/CreateSequence.java b/src/main/java/net/sf/jsqlparser/statement/create/sequence/CreateSequence.java index 0d1c2b7f75..f005e3b0d1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/sequence/CreateSequence.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/sequence/CreateSequence.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement.create.sequence; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Sequence; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; @@ -16,9 +17,44 @@ /** * A {@code CREATE SEQUENCE} statement */ -public class CreateSequence implements Statement { +public class CreateSequence extends ASTNodeAccessImpl implements Statement { + + public enum Persistence { + TEMP, TEMPORARY, UNLOGGED, LOCAL_TEMP, LOCAL_TEMPORARY, GLOBAL_TEMP, GLOBAL_TEMPORARY + } public Sequence sequence; + private Persistence persistence; + private boolean ifNotExists; + + public Persistence getPersistence() { + return persistence; + } + + public void setPersistence(Persistence persistence) { + this.persistence = persistence; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CREATE "); + if (persistence != null) { + builder.append(persistence.name().replace('_', ' ')).append(' '); + } + builder.append("SEQUENCE "); + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + return builder.append(sequence); + } + public Sequence getSequence() { return sequence; @@ -35,9 +71,7 @@ public T accept(StatementVisitor statementVisitor, S context) { @Override public String toString() { - String sql; - sql = "CREATE SEQUENCE " + sequence; - return sql; + return appendTo(new StringBuilder()).toString(); } public CreateSequence withSequence(Sequence sequence) { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/server/CreateServer.java b/src/main/java/net/sf/jsqlparser/statement/create/server/CreateServer.java new file mode 100644 index 0000000000..e4ebc4f2ed --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/server/CreateServer.java @@ -0,0 +1,96 @@ +/*- + * #%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.statement.create.server; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; +import net.sf.jsqlparser.expression.StringValue; + +public class CreateServer extends ForeignDataStatement { + private String name; + private boolean ifNotExists; + private StringValue type; + private Expression version; + private String foreignDataWrapper; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean value) { + ifNotExists = value; + } + + public StringValue getType() { + return type; + } + + public void setType(StringValue value) { + type = value; + } + + public Expression getVersion() { + return version; + } + + public void setVersion(Expression value) { + version = value; + } + + public String getForeignDataWrapper() { + return foreignDataWrapper; + } + + public void setForeignDataWrapper(String value) { + foreignDataWrapper = value; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("CREATE SERVER "); + if (ifNotExists) { + sql.append("IF NOT EXISTS "); + } + sql.append(name); + if (type != null) { + sql.append(" TYPE "); + printer.accept(type); + } + if (version != null) { + sql.append(" VERSION "); + printer.accept(version); + } + sql.append(" FOREIGN DATA WRAPPER ").append(foreignDataWrapper); + appendOptionsTo(sql, printer); + return sql; + } + + @Override + public void visitExpressions(Consumer visitor) { + super.visitExpressions(visitor); + visit(type, visitor); + visit(version, visitor); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/statistics/CreateStatistics.java b/src/main/java/net/sf/jsqlparser/statement/create/statistics/CreateStatistics.java new file mode 100644 index 0000000000..df966df81c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/statistics/CreateStatistics.java @@ -0,0 +1,112 @@ +/*- + * #%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.statement.create.statistics; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** PostgreSQL extended statistics over columns and parenthesized expressions. */ +public class CreateStatistics extends ASTNodeAccessImpl implements Statement { + public enum Kind { + NDISTINCT, DEPENDENCIES, MCV + } + + private String name; + private boolean ifNotExists = false; + private List kinds = new ArrayList<>(); + private ExpressionList expressions = new ExpressionList<>(); + private Table table; + + public String getName() { + return name; + } + + public CreateStatistics setName(String name) { + this.name = name; + return this; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public CreateStatistics setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + return this; + } + + public List getKinds() { + return kinds; + } + + public CreateStatistics setKinds(List kinds) { + this.kinds = kinds; + return this; + } + + public ExpressionList getExpressions() { + return expressions; + } + + public CreateStatistics setExpressions(ExpressionList expressions) { + this.expressions = expressions; + return this; + } + + public Table getTable() { + return table; + } + + public CreateStatistics setTable(Table table) { + this.table = table; + return this; + } + + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("CREATE STATISTICS"); + if (ifNotExists) { + sql.append(" IF NOT EXISTS"); + } + if (name != null) { + sql.append(' ').append(name); + } + if (kinds != null && !kinds.isEmpty()) { + sql.append(" ("); + for (int i = 0; i < kinds.size(); i++) { + if (i > 0) { + sql.append(", "); + } + sql.append(kinds.get(i).name().toLowerCase(java.util.Locale.ROOT)); + } + sql.append(')'); + } + sql.append(" ON "); + printer.accept(expressions); + return sql.append(" FROM ").append(table); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/subscription/CreateSubscription.java b/src/main/java/net/sf/jsqlparser/statement/create/subscription/CreateSubscription.java new file mode 100644 index 0000000000..b67dda66c1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/subscription/CreateSubscription.java @@ -0,0 +1,86 @@ +/*- + * #%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.statement.create.subscription; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class CreateSubscription extends ASTNodeAccessImpl implements Statement { + private String name; + private StringValue connection; + private List publications = new ArrayList<>(); + private List options = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public StringValue getConnection() { + return connection; + } + + public void setConnection(StringValue connection) { + this.connection = connection; + } + + public List getPublications() { + return publications; + } + + public void setPublications(List publications) { + this.publications = publications; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + public void appendTo(StringBuilder sql, Consumer expressions) { + sql.append("CREATE SUBSCRIPTION ").append(name).append(" CONNECTION "); + expressions.accept(connection); + sql.append(" PUBLICATION ").append(String.join(", ", publications)); + if (!options.isEmpty()) { + sql.append(" WITH ("); + for (int i = 0; i < options.size(); i++) { + if (i > 0) { + sql.append(", "); + } + options.get(i).appendTo(sql, expressions); + } + sql.append(')'); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/subscription/SubscriptionOption.java b/src/main/java/net/sf/jsqlparser/statement/create/subscription/SubscriptionOption.java new file mode 100644 index 0000000000..62cad87089 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/subscription/SubscriptionOption.java @@ -0,0 +1,70 @@ +/*- + * #%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.statement.create.subscription; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.replication.ReplicationOption; + +public class SubscriptionOption extends ReplicationOption { + public enum Kind { + CONNECT, CREATE_SLOT, ENABLED, SLOT_NAME, BINARY, COPY_DATA, STREAMING, SYNCHRONOUS_COMMIT, TWO_PHASE, DISABLE_ON_ERROR, PASSWORD_REQUIRED, RUN_AS_OWNER, ORIGIN, FAILOVER, REFRESH, LSN + } + public enum Streaming { + ON, OFF, PARALLEL + } + public enum Origin { + ANY, NONE + } + public enum SynchronousCommit { + OFF, LOCAL, REMOTE_WRITE, ON, REMOTE_APPLY + } + + public SubscriptionOption(String name, Kind kind, Expression value) { + super(name, kind, value); + } + + public Boolean getBooleanValue() { + switch (getKind()) { + case CONNECT: + case CREATE_SLOT: + case ENABLED: + case BINARY: + case COPY_DATA: + case TWO_PHASE: + case DISABLE_ON_ERROR: + case PASSWORD_REQUIRED: + case RUN_AS_OWNER: + case FAILOVER: + case REFRESH: + return booleanValue(); + default: + return null; + } + } + + public Streaming getStreaming() { + return getKind() == Kind.STREAMING ? enumValue(Streaming.class) : null; + } + + public Origin getOrigin() { + return getKind() == Kind.ORIGIN ? enumValue(Origin.class) : null; + } + + public SynchronousCommit getSynchronousCommit() { + return getKind() == Kind.SYNCHRONOUS_COMMIT ? enumValue(SynchronousCommit.class) : null; + } + + /** Quoted 'NONE' remains a literal slot name, not the NONE keyword. */ + public boolean isSlotNameNone() { + return getKind() == Kind.SLOT_NAME && !(getValue() instanceof StringValue) + && "NONE".equalsIgnoreCase(getValueText()); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/synonym/CreateSynonym.java b/src/main/java/net/sf/jsqlparser/statement/create/synonym/CreateSynonym.java index ac7eb2640b..9411ba577b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/synonym/CreateSynonym.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/synonym/CreateSynonym.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement.create.synonym; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Synonym; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; @@ -16,7 +17,7 @@ import java.util.ArrayList; import java.util.List; -public class CreateSynonym implements Statement { +public class CreateSynonym extends ASTNodeAccessImpl implements Statement { public Synonym synonym; private boolean orReplace; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java index 254fb3d01e..67a42ba16d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java @@ -9,8 +9,8 @@ */ package net.sf.jsqlparser.statement.create.table; -import java.util.Collection; import java.util.List; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.schema.Table; @@ -21,6 +21,17 @@ public class CheckConstraint extends NamedConstraint { private Expression expression; + private boolean noInherit; + + public CheckConstraint() { + setType("CHECK"); + } + + @Override + public ConstraintKind getKind() { + return ConstraintKind.CHECK; + } + public Table getTable() { return table; } @@ -37,14 +48,51 @@ public void setExpression(Expression expression) { this.expression = expression; } + /** Whether PostgreSQL should keep this CHECK from being inherited by child tables. */ + public boolean isNoInherit() { + return noInherit; + } + + public void setNoInherit(boolean noInherit) { + this.noInherit = noInherit; + } + + public CheckConstraint withNoInherit(boolean noInherit) { + setNoInherit(noInherit); + return this; + } + + public Boolean getEnforced() { + return getConstraintAttributes() == null ? null : getConstraintAttributes().getEnforced(); + } + + public void setEnforced(Boolean enforced) { + ConstraintAttributes attributes = getConstraintAttributes(); + if (attributes == null) { + if (enforced == null) { + return; + } + attributes = new ConstraintAttributes(); + setConstraintAttributes(attributes); + } + attributes.setEnforced(enforced); + } + @Override - public String toString() { - StringBuilder b = new StringBuilder(); - if (getName() != null) { - b.append("CONSTRAINT ").append(getName()).append(" "); + public void appendTo(StringBuilder b, Consumer expressionPrinter) { + appendConstraintPrefixTo(b); + b.append("CHECK ("); + if (expression == null) { + b.append("null"); + } else { + expressionPrinter.accept(expression); } - b.append("CHECK (").append(expression).append(")"); - return b.toString(); + b.append(')'); + if (noInherit) { + b.append(" NO INHERIT"); + } + appendConstraintSuffixTo(b); + appendConstraintAttributesTo(b); } public CheckConstraint withTable(Table table) { @@ -57,6 +105,11 @@ public CheckConstraint withExpression(Expression expression) { return this; } + public CheckConstraint withEnforced(Boolean enforced) { + setEnforced(enforced); + return this; + } + public E getExpression(Class type) { return type.cast(getExpression()); } @@ -67,8 +120,8 @@ public CheckConstraint withType(String type) { } @Override - public CheckConstraint withUsing(String using) { - return (CheckConstraint) super.withUsing(using); + public CheckConstraint withUseConstraintKeyword(boolean useConstraintKeyword) { + return (CheckConstraint) super.withUseConstraintKeyword(useConstraintKeyword); } @Override @@ -81,33 +134,4 @@ public CheckConstraint withName(String name) { return (CheckConstraint) super.withName(name); } - @Override - public CheckConstraint withColumnsNames(List list) { - return (CheckConstraint) super.withColumnsNames(list); - } - - @Override - public CheckConstraint withColumns(List columns) { - return (CheckConstraint) super.withColumns(columns); - } - - @Override - public CheckConstraint addColumns(ColumnParams... functionDeclarationParts) { - return (CheckConstraint) super.addColumns(functionDeclarationParts); - } - - @Override - public CheckConstraint addColumns(Collection functionDeclarationParts) { - return (CheckConstraint) super.addColumns(functionDeclarationParts); - } - - @Override - public CheckConstraint withIndexSpec(List idxSpec) { - return (CheckConstraint) super.withIndexSpec(idxSpec); - } - - @Override - public CheckConstraint withIndexKeyword(String indexKeyword) { - return (CheckConstraint) super.withIndexKeyword(indexKeyword); - } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java index a3ddcdb9fc..ff90bf875d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java @@ -10,9 +10,10 @@ package net.sf.jsqlparser.statement.create.table; import net.sf.jsqlparser.expression.IntervalQualifier; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.select.PlainSelect; -import java.io.Serializable; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -22,27 +23,91 @@ import static java.util.stream.Collectors.joining; -public class ColDataType implements Serializable { +public class ColDataType extends ASTNodeAccessImpl { + + public enum Signedness { + SIGNED, UNSIGNED + } + + public enum TypeModifier { + SIGNED, UNSIGNED, ZEROFILL + } + + public enum NationalCharacterType { + CHAR, VARCHAR + } + + /** Spelling of a character set clause; MySQL shorthands also select a character set. */ + public enum CharacterSetSyntax { + CHARACTER_SET, CHARSET, ASCII, UNICODE + } private String dataType; private List argumentsStringList; private String characterSet; + private CharacterSetSyntax characterSetSyntax = CharacterSetSyntax.CHARACTER_SET; private IntervalQualifier intervalQualifier; private List arrayData = new ArrayList(); + private boolean usingArrayKeyword; + + /** ARRAY or ARRAY[n] spelling; arrayData still contains the single dimension. */ + public boolean isUsingArrayKeyword() { + return usingArrayKeyword; + } + + public void setUsingArrayKeyword(boolean usingArrayKeyword) { + this.usingArrayKeyword = usingArrayKeyword; + } + + private Signedness signedness; + private boolean zerofill; + private BigInteger precision; + private boolean maxPrecision; + private Integer scale; + private List typeModifiers; + private NationalCharacterType nationalCharacterType; + private XmlTypeModifier xmlTypeModifier; public ColDataType() { // empty constructor } public ColDataType(String dataType, int precision, int scale) { - this.dataType = dataType; + this(dataType); + setNumericTypeParameters(precision < 0 ? null + : precision == Integer.MAX_VALUE ? "MAX" : Integer.toString(precision), + scale < 0 ? null : Integer.valueOf(scale)); + } - if (precision >= 0) { - this.dataType += " (" + (precision == Integer.MAX_VALUE ? "MAX" : precision); - if (scale >= 0) { - this.dataType += ", " + scale; - } - this.dataType += ")"; + /** + * Creates a parameterized type, using {@code null} for an omitted parameter. Unlike the legacy + * primitive constructor, this accepts negative scales, including {@code -1}. The legacy + * {@link Integer#MAX_VALUE} precision sentinel continues to represent MAX. + */ + public static ColDataType fromNumericParameters(String dataType, Integer precision, + Integer scale) { + return fromTypeParameters(dataType, precision == null ? null + : precision == Integer.MAX_VALUE ? "MAX" : precision.toString(), scale); + } + + /** + * Creates a type from its numeric parameter spelling. Unlike the legacy primitive constructor, + * a numeric 2147483647 is distinct from MAX and larger lengths are retained without narrowing. + */ + public static ColDataType fromTypeParameters(String dataType, String precision, Integer scale) { + ColDataType type = new ColDataType(dataType); + type.setNumericTypeParameters(precision, scale); + return type; + } + + private void setNumericTypeParameters(String parameter, Integer scale) { + if (parameter != null) { + maxPrecision = "MAX".equalsIgnoreCase(parameter); + // Oracle's NUMBER(*, s) has no precision of its own. + precision = maxPrecision || "*".equals(parameter) ? null : new BigInteger(parameter); + this.scale = scale; + dataType += " (" + (maxPrecision ? "MAX" : precision != null ? precision : parameter) + + (scale != null ? ", " + scale : "") + ")"; } } @@ -62,6 +127,54 @@ public String getDataType() { return dataType; } + /** + * Returns the type name without parenthesized parameters, retaining qualification, quoting and + * multiword names such as TIMESTAMP WITH TIME ZONE. The legacy {@link #getDataType()} spelling + * is unchanged. Quoted parentheses are part of an identifier and are retained. + */ + public String getBaseTypeName() { + if (dataType == null) { + return null; + } + StringBuilder name = new StringBuilder(); + int depth = 0; + char quote = 0; + for (int i = 0; i < dataType.length(); i++) { + char c = dataType.charAt(i); + if (quote != 0) { + if (depth == 0) { + name.append(c); + } + if (c == quote) { + if (i + 1 < dataType.length() && dataType.charAt(i + 1) == quote) { + if (depth == 0) { + name.append(quote); + } + i++; + } else { + quote = 0; + } + } + } else if (c == '\'' || c == '"' || c == '`' || c == '[') { + quote = c == '[' ? ']' : c; + if (depth == 0) { + name.append(c); + } + } else if (c == '(') { + depth++; + while (name.length() > 0 + && Character.isWhitespace(name.charAt(name.length() - 1))) { + name.setLength(name.length() - 1); + } + } else if (c == ')' && depth > 0) { + depth--; + } else if (depth == 0) { + name.append(c); + } + } + return name.toString().trim(); + } + public void setDataType(String string) { dataType = string; } @@ -70,12 +183,51 @@ public void setDataType(List list) { dataType = list.stream().collect(joining(".")); } + /** Returns the character set name, resolving ASCII to latin1 and UNICODE to ucs2. */ public String getCharacterSet() { return characterSet; } public void setCharacterSet(String characterSet) { this.characterSet = characterSet; + if (characterSetSyntax == CharacterSetSyntax.ASCII + || characterSetSyntax == CharacterSetSyntax.UNICODE) { + characterSetSyntax = CharacterSetSyntax.CHARACTER_SET; + } + } + + public CharacterSetSyntax getCharacterSetSyntax() { + return characterSetSyntax; + } + + /** + * Selects the clause spelling. ASCII selects latin1 and UNICODE selects ucs2; the explicit + * spellings retain the current character set. Calling {@link #setCharacterSet(String)} after a + * shorthand switches to CHARACTER SET. + */ + public void setCharacterSetSyntax(CharacterSetSyntax characterSetSyntax) { + this.characterSetSyntax = Objects.requireNonNull(characterSetSyntax, "characterSetSyntax"); + if (characterSetSyntax == CharacterSetSyntax.ASCII) { + characterSet = "latin1"; + } else if (characterSetSyntax == CharacterSetSyntax.UNICODE) { + characterSet = "ucs2"; + } + } + + public ColDataType withCharacterSetSyntax(CharacterSetSyntax characterSetSyntax) { + setCharacterSetSyntax(characterSetSyntax); + return this; + } + + /** Whether the character set clause uses MySQL's CHARSET abbreviation. */ + public boolean isUseCharsetKeyword() { + return characterSetSyntax == CharacterSetSyntax.CHARSET; + } + + public void setUseCharsetKeyword(boolean useCharsetKeyword) { + setCharacterSetSyntax(useCharsetKeyword + ? CharacterSetSyntax.CHARSET + : CharacterSetSyntax.CHARACTER_SET); } public IntervalQualifier getIntervalQualifier() { @@ -94,10 +246,145 @@ public void setArrayData(List arrayData) { this.arrayData = arrayData; } + public Signedness getSignedness() { + return signedness; + } + + public void setSignedness(Signedness signedness) { + this.signedness = signedness; + } + + public boolean isZerofill() { + return zerofill; + } + + public void setZerofill(boolean zerofill) { + this.zerofill = zerofill; + } + + /** Returns MySQL numeric modifiers in their original order, including repetitions. */ + public List getTypeModifiers() { + return typeModifiers; + } + + public void setTypeModifiers(List typeModifiers) { + this.typeModifiers = typeModifiers; + signedness = null; + zerofill = false; + if (typeModifiers != null) { + for (TypeModifier modifier : typeModifiers) { + updateEffectiveModifier(modifier); + } + } + } + + public void addTypeModifier(TypeModifier typeModifier) { + if (typeModifiers == null) { + typeModifiers = new ArrayList<>(); + } + typeModifiers.add(typeModifier); + updateEffectiveModifier(typeModifier); + } + + private void updateEffectiveModifier(TypeModifier modifier) { + switch (modifier) { + case SIGNED: + signedness = Signedness.SIGNED; + break; + case UNSIGNED: + signedness = Signedness.UNSIGNED; + break; + case ZEROFILL: + zerofill = true; + break; + default: + break; + } + } + + public NationalCharacterType getNationalCharacterType() { + return nationalCharacterType; + } + + public void setNationalCharacterType(NationalCharacterType nationalCharacterType) { + this.nationalCharacterType = nationalCharacterType; + } + + public boolean isNational() { + return nationalCharacterType != null; + } + + public XmlTypeModifier getXmlTypeModifier() { + return xmlTypeModifier; + } + + public void setXmlTypeModifier(XmlTypeModifier xmlTypeModifier) { + this.xmlTypeModifier = xmlTypeModifier; + } + + public ColDataType withXmlTypeModifier(XmlTypeModifier xmlTypeModifier) { + setXmlTypeModifier(xmlTypeModifier); + return this; + } + + /** + * The first numeric type parameter, e.g. {@code 255} for {@code VARCHAR(255)} or {@code 10} for + * {@code DECIMAL(10, 2)}. {@code MAX} is reported as {@link Integer#MAX_VALUE}. Returns + * {@code null} when the type carries no numeric parameters, e.g. {@code INT} or + * {@code ENUM('a', 'b')}, when the precision is Oracle's {@code *} as in {@code NUMBER(*, 0)}, + * or when a numeric length exceeds the integer range. Use {@link #getNumericPrecision()} for + * the full range and {@link #isMaxPrecision()} to distinguish MAX from the numeric value + * 2147483647. + */ + public Integer getPrecision() { + return maxPrecision ? Integer.valueOf(Integer.MAX_VALUE) + : precision != null && precision.bitLength() < Integer.SIZE + ? Integer.valueOf(precision.intValue()) + : null; + } + + public void setPrecision(Integer precision) { + setNumericPrecision(precision == null ? null : BigInteger.valueOf(precision)); + } + + /** Returns the exact numeric parameter, or null for an omitted parameter or MAX. */ + public BigInteger getNumericPrecision() { + return precision; + } + + /** Updates numeric metadata without changing the legacy rendered type spelling. */ + public void setNumericPrecision(BigInteger precision) { + this.precision = precision; + maxPrecision = false; + } + + /** Distinguishes the MAX keyword from an equal numeric value in the legacy accessor. */ + public boolean isMaxPrecision() { + return maxPrecision; + } + + /** + * The second numeric type parameter, e.g. {@code 2} for {@code DECIMAL(10, 2)} or {@code -3} + * for PostgreSQL {@code NUMERIC(2, -3)}. Returns {@code null} when absent. + */ + public Integer getScale() { + return scale; + } + + public void setScale(Integer scale) { + this.scale = scale; + } + @Override public String toString() { StringBuilder arraySpec = new StringBuilder(); + if (usingArrayKeyword && !arrayData.isEmpty()) { + arraySpec.append(" ARRAY"); + } for (Integer item : arrayData) { + if (usingArrayKeyword && item == null) { + continue; + } arraySpec.append("["); if (item != null) { arraySpec.append(item); @@ -106,11 +393,31 @@ public String toString() { } return dataType + (intervalQualifier != null ? " " + intervalQualifier.toString() : "") + + (xmlTypeModifier != null ? " " + xmlTypeModifier : "") + (argumentsStringList != null ? " " + PlainSelect.getStringList(argumentsStringList, true, true) : "") + + (typeModifiers != null && !typeModifiers.isEmpty() + ? " " + PlainSelect.getStringList(typeModifiers, false, false) + : (signedness != null ? " " + signedness : "") + + (zerofill ? " ZEROFILL" : "")) + arraySpec.toString() - + (characterSet != null ? " CHARACTER SET " + characterSet : ""); + + characterSetClause(); + } + + private String characterSetClause() { + if (characterSet == null) { + return ""; + } + switch (characterSetSyntax) { + case ASCII: + case UNICODE: + return " " + characterSetSyntax; + case CHARSET: + return " CHARSET " + characterSet; + default: + return " CHARACTER SET " + characterSet; + } } public ColDataType withDataType(String dataType) { @@ -138,6 +445,36 @@ public ColDataType withArrayData(List arrayData) { return this; } + public ColDataType withSignedness(Signedness signedness) { + setSignedness(signedness); + return this; + } + + public ColDataType withZerofill(boolean zerofill) { + setZerofill(zerofill); + return this; + } + + public ColDataType withTypeModifiers(List typeModifiers) { + setTypeModifiers(typeModifiers); + return this; + } + + public ColDataType withNationalCharacterType(NationalCharacterType nationalCharacterType) { + setNationalCharacterType(nationalCharacterType); + return this; + } + + public ColDataType withPrecision(Integer precision) { + this.setPrecision(precision); + return this; + } + + public ColDataType withScale(Integer scale) { + this.setScale(scale); + return this; + } + public ColDataType addArgumentsStringList(String... argumentsStringList) { List collection = Optional.ofNullable(getArgumentsStringList()).orElseGet(ArrayList::new); @@ -177,17 +514,34 @@ public final boolean equals(Object o) { return dataType.equalsIgnoreCase(that.dataType) && Objects.equals(argumentsStringList, that.argumentsStringList) && Objects.equals(characterSet, that.characterSet) + && characterSetSyntax == that.characterSetSyntax && Objects.equals(intervalQualifier, that.intervalQualifier) - && Objects.equals(arrayData, that.arrayData); + && Objects.equals(arrayData, that.arrayData) + && usingArrayKeyword == that.usingArrayKeyword + && signedness == that.signedness + && zerofill == that.zerofill + && Objects.equals(typeModifiers, that.typeModifiers) + && Objects.equals(xmlTypeModifier, that.xmlTypeModifier) + && nationalCharacterType == that.nationalCharacterType; } @Override public int hashCode() { - int result = dataType.hashCode(); + // Use the same per-code-point case folding as String.equalsIgnoreCase, including Unicode. + int result = dataType.codePoints() + .map(c -> Character.toLowerCase(Character.toUpperCase(c))) + .reduce(0, (hash, c) -> 31 * hash + c); result = 31 * result + Objects.hashCode(argumentsStringList); result = 31 * result + Objects.hashCode(characterSet); + result = 31 * result + Objects.hashCode(characterSetSyntax); result = 31 * result + Objects.hashCode(intervalQualifier); result = 31 * result + Objects.hashCode(arrayData); + result = 31 * result + Boolean.hashCode(usingArrayKeyword); + result = 31 * result + Objects.hashCode(signedness); + result = 31 * result + Boolean.hashCode(zerofill); + result = 31 * result + Objects.hashCode(typeModifiers); + result = 31 * result + Objects.hashCode(nationalCharacterType); + result = 31 * result + Objects.hashCode(xmlTypeModifier); return result; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java index a7f47104c3..bf3d26f583 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java @@ -10,23 +10,34 @@ package net.sf.jsqlparser.statement.create.table; import net.sf.jsqlparser.statement.imprt.ImportColumn; -import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.expression.Expression; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Optional; +import java.util.Objects; +import java.util.function.Consumer; /** * Globally used definition class for columns. */ -public class ColumnDefinition implements ImportColumn, Serializable { +public class ColumnDefinition implements ImportColumn, TableElement, Serializable { private String columnName; private ColDataType colDataType; - private List columnSpecs; + private List columnOptions; + private boolean withOptions; + + public boolean isWithOptions() { + return withOptions; + } + + public void setWithOptions(boolean withOptions) { + this.withOptions = withOptions; + } public ColumnDefinition() {} @@ -37,15 +48,84 @@ public ColumnDefinition(String columnName, ColDataType colDataType) { public ColumnDefinition(String columnName, ColDataType colDataType, List columnSpecs) { this(columnName, colDataType); - this.columnSpecs = columnSpecs; + columnOptions = rawOptions(columnSpecs); } + /** + * Returns an unmodifiable token snapshot for both raw and structured options, or null when + * options are unset. Use {@link #getColumnOptions()} to edit the nodes, or + * {@link #addColumnSpecs(Collection)} to append without discarding structured expressions, + * references and constraints. DEFAULT values use the expression's SQL rendering. + */ public List getColumnSpecs() { - return columnSpecs; + if (columnOptions != null) { + List tokens = new ArrayList<>(); + for (ColumnOption option : columnOptions) { + tokens.addAll(option.getTokens()); + } + return Collections.unmodifiableList(tokens); + } + return null; } + /** + * Replaces every option with opaque tokens, without parsing them. The supplied list is copied; + * null clears the options, while an empty list stores an explicitly empty option list. + */ public void setColumnSpecs(List list) { - columnSpecs = list; + columnOptions = rawOptions(list); + } + + private static List rawOptions(List list) { + List options = list == null ? null : new ArrayList<>(); + if (list != null && !list.isEmpty()) { + options.add(ColumnOption.raw(new ArrayList<>(list))); + } + return options; + } + + /** + * Returns the mutable option list in source order, or null when options are unset. Opaque + * specifications are included as {@link ColumnOption.Kind#OTHER} nodes. Structured option + * keywords use canonical capitalization when rendered. + */ + public List getColumnOptions() { + return columnOptions; + } + + /** Copies the list membership while retaining the supplied mutable option nodes. */ + public void setColumnOptions(List columnOptions) { + this.columnOptions = columnOptions == null ? null : new ArrayList<>(columnOptions); + } + + public boolean isSerialDefaultValue() { + return columnOptions != null && columnOptions.stream() + .anyMatch(option -> option.getKind() == ColumnOption.Kind.SERIAL_DEFAULT_VALUE); + } + + public ForeignKeyReference getForeignKeyReference() { + if (columnOptions == null) { + return null; + } + return columnOptions.stream() + .filter(option -> option.getKind() == ColumnOption.Kind.REFERENCE) + .map(ColumnOption::getForeignKeyReference) + .findFirst() + .orElse(null); + } + + public ColumnDefinition withColumnOptions(List columnOptions) { + setColumnOptions(columnOptions); + return this; + } + + public ColumnDefinition addColumnOptions(ColumnOption... columnOptions) { + Objects.requireNonNull(columnOptions, "columnOptions"); + if (this.columnOptions == null) { + this.columnOptions = new ArrayList<>(); + } + Collections.addAll(this.columnOptions, columnOptions); + return this; } public ColDataType getColDataType() { @@ -66,14 +146,40 @@ public void setColumnName(String string) { @Override public String toString() { - return columnName + " " + toStringDataTypeAndSpec(); + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString().trim(); + } + + /** Appends a column definition using the supplied printer for structured expressions. */ + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append(columnName); + if (colDataType != null || withOptions) { + builder.append(' '); + } + appendDataTypeAndSpecTo(builder, expressionPrinter); } public String toStringDataTypeAndSpec() { - return (colDataType == null ? "" : colDataType) - + (columnSpecs != null && !columnSpecs.isEmpty() - ? " " + PlainSelect.getStringList(columnSpecs, false, false) - : ""); + StringBuilder builder = new StringBuilder(); + appendDataTypeAndSpecTo(builder, builder::append); + return builder.toString(); + } + + protected void appendDataTypeAndSpecTo(StringBuilder builder, + Consumer expressionPrinter) { + if (colDataType != null) { + builder.append(colDataType); + } + if (withOptions) { + builder.append("WITH OPTIONS"); + } + if (columnOptions != null) { + for (ColumnOption option : columnOptions) { + builder.append(' '); + option.appendTo(builder, expressionPrinter); + } + } } public ColumnDefinition withColumnName(String columnName) { @@ -92,14 +198,18 @@ public ColumnDefinition withColumnSpecs(List columnSpecs) { } public ColumnDefinition addColumnSpecs(String... columnSpecs) { - List collection = Optional.ofNullable(getColumnSpecs()).orElseGet(ArrayList::new); - Collections.addAll(collection, columnSpecs); - return this.withColumnSpecs(collection); + return addColumnSpecs(Arrays.asList(columnSpecs)); } + /** Appends copied opaque tokens after the existing nodes, preserving their structure. */ public ColumnDefinition addColumnSpecs(Collection columnSpecs) { - List collection = Optional.ofNullable(getColumnSpecs()).orElseGet(ArrayList::new); - collection.addAll(columnSpecs); - return this.withColumnSpecs(collection); + Objects.requireNonNull(columnSpecs, "columnSpecs"); + if (columnOptions == null) { + columnOptions = new ArrayList<>(); + } + if (!columnSpecs.isEmpty()) { + columnOptions.add(ColumnOption.raw(new ArrayList<>(columnSpecs))); + } + return this; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java new file mode 100644 index 0000000000..f0e7fec94f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java @@ -0,0 +1,532 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** A structured option following a column data type. */ +public class ColumnOption implements Serializable { + + public enum Kind { + SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, NULLABILITY, COLLATE, COMMENT, ON_UPDATE, GENERATED, AUTO_INCREMENT, VISIBILITY, STORAGE, COMPRESSION, FOREIGN_OPTIONS, SRID, COLUMN_FORMAT, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, OTHER + } + + /** PostgreSQL storage strategies and MySQL column storage locations. */ + public enum Storage { + PLAIN, EXTERNAL, EXTENDED, MAIN, DEFAULT, DISK, MEMORY + } + + /** MySQL {@code COLUMN_FORMAT} values. */ + public enum ColumnFormat { + FIXED, DYNAMIC, DEFAULT + } + + /** + * Inline key spellings whose legacy tokens are split on spaces. KEY is MySQL's column-level + * synonym for PRIMARY KEY. + */ + private static final List KEY_SPELLINGS = + Arrays.asList("PRIMARY KEY", "UNIQUE KEY", "KEY"); + + private List foreignOptions; + private Long srid; + private ColumnFormat columnFormat; + private StringValue engineAttribute; + private boolean useEquals; + + public List getForeignOptions() { + return foreignOptions; + } + + public void setForeignOptions(List options) { + foreignOptions = options; + kind = Kind.FOREIGN_OPTIONS; + tokens = null; + } + + public static ColumnOption foreignOptions(List options) { + ColumnOption option = new ColumnOption(); + option.setForeignOptions(options); + return option; + } + + private Storage storage; + private String compression; + + public static ColumnOption storage(Storage storage) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.STORAGE; + option.setStorage(storage); + return option; + } + + public Storage getStorage() { + return storage; + } + + public void setStorage(Storage storage) { + this.storage = Objects.requireNonNull(storage, "storage"); + } + + public static ColumnOption compression(String compression) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.COMPRESSION; + option.setCompression(compression); + return option; + } + + public String getCompression() { + return compression; + } + + public void setCompression(String compression) { + this.compression = Objects.requireNonNull(compression, "compression"); + } + + /** Creates a MySQL {@code SRID} option for a spatial column. */ + public static ColumnOption srid(long srid) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.SRID; + option.setSrid(srid); + return option; + } + + /** Returns the spatial reference system identifier of an SRID option, or null. */ + public Long getSrid() { + return srid; + } + + public void setSrid(long srid) { + this.srid = srid; + } + + /** Creates a MySQL {@code COLUMN_FORMAT} option. */ + public static ColumnOption columnFormat(ColumnFormat columnFormat) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.COLUMN_FORMAT; + option.setColumnFormat(columnFormat); + return option; + } + + public ColumnFormat getColumnFormat() { + return columnFormat; + } + + public void setColumnFormat(ColumnFormat columnFormat) { + this.columnFormat = Objects.requireNonNull(columnFormat, "columnFormat"); + } + + /** Creates a MySQL {@code ENGINE_ATTRIBUTE} option with a JSON string literal. */ + public static ColumnOption engineAttribute(StringValue attribute) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.ENGINE_ATTRIBUTE; + option.setEngineAttribute(attribute); + return option; + } + + /** Creates a MySQL {@code SECONDARY_ENGINE_ATTRIBUTE} option with a JSON string literal. */ + public static ColumnOption secondaryEngineAttribute(StringValue attribute) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.SECONDARY_ENGINE_ATTRIBUTE; + option.setEngineAttribute(attribute); + return option; + } + + /** Returns the literal of an ENGINE_ATTRIBUTE or SECONDARY_ENGINE_ATTRIBUTE option, or null. */ + public StringValue getEngineAttribute() { + return engineAttribute; + } + + public void setEngineAttribute(StringValue attribute) { + engineAttribute = Objects.requireNonNull(attribute, "attribute"); + } + + /** Whether an engine attribute is written with the optional equals sign. */ + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + private boolean isEngineAttribute() { + return kind == Kind.ENGINE_ATTRIBUTE || kind == Kind.SECONDARY_ENGINE_ATTRIBUTE; + } + + private Kind kind = Kind.OTHER; + private List tokens; + private ForeignKeyReference foreignKeyReference; + private IdentityDefinition identityDefinition; + private NamedConstraint constraint; + private Expression defaultExpression; + private Boolean nullable; + private Boolean visible; + private String collation; + private StringValue comment; + private Expression onUpdateExpression; + private GeneratedColumnDefinition generatedDefinition; + + public static ColumnOption nullability(boolean nullable) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.NULLABILITY; + option.nullable = nullable; + return option; + } + + public Boolean getNullable() { + return nullable; + } + + public void setNullable(boolean nullable) { + this.nullable = nullable; + } + + public static ColumnOption visibility(boolean visible) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.VISIBILITY; + option.visible = visible; + return option; + } + + public Boolean getVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public static ColumnOption autoIncrement() { + ColumnOption option = new ColumnOption(); + option.kind = Kind.AUTO_INCREMENT; + return option; + } + + public static ColumnOption collate(String collation) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.COLLATE; + option.setCollation(collation); + return option; + } + + public String getCollation() { + return collation; + } + + public void setCollation(String collation) { + this.collation = Objects.requireNonNull(collation, "collation"); + } + + public static ColumnOption comment(StringValue comment) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.COMMENT; + option.setComment(comment); + return option; + } + + public StringValue getComment() { + return comment; + } + + public void setComment(StringValue comment) { + this.comment = Objects.requireNonNull(comment, "comment"); + } + + public static ColumnOption onUpdate(Expression expression) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.ON_UPDATE; + option.setOnUpdateExpression(expression); + return option; + } + + public Expression getOnUpdateExpression() { + return onUpdateExpression; + } + + public void setOnUpdateExpression(Expression expression) { + onUpdateExpression = Objects.requireNonNull(expression, "expression"); + } + + public static ColumnOption generated(GeneratedColumnDefinition definition) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.GENERATED; + option.setGeneratedDefinition(definition); + return option; + } + + public GeneratedColumnDefinition getGeneratedDefinition() { + return generatedDefinition; + } + + public void setGeneratedDefinition(GeneratedColumnDefinition definition) { + generatedDefinition = Objects.requireNonNull(definition, "definition"); + } + + /** Visits expressions of the selected option kind, including comment and attribute literals. */ + public void visitExpressions(Consumer visitor) { + if (kind == Kind.FOREIGN_OPTIONS) { + ForeignDataOption.visitExpressions(foreignOptions, visitor); + } else if (kind == Kind.DEFAULT) { + visitor.accept(defaultExpression); + } else if (kind == Kind.COMMENT) { + visitor.accept(comment); + } else if (isEngineAttribute()) { + visitor.accept(engineAttribute); + } else if (kind == Kind.ON_UPDATE) { + visitor.accept(onUpdateExpression); + } else if (kind == Kind.GENERATED) { + visitor.accept(generatedDefinition.getExpression()); + } + } + + /** Creates a DEFAULT option. Use a NullValue expression for SQL NULL. */ + public static ColumnOption defaultValue(Expression expression) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.DEFAULT; + option.setDefaultExpression(expression); + return option; + } + + public Expression getDefaultExpression() { + return defaultExpression; + } + + /** Replaces the expression of a DEFAULT option created by {@link #defaultValue(Expression)}. */ + public void setDefaultExpression(Expression expression) { + defaultExpression = Objects.requireNonNull(expression, "defaultExpression"); + } + + public static ColumnOption identity(IdentityDefinition definition) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.IDENTITY; + option.identityDefinition = definition; + return option; + } + + public static ColumnOption constraint(NamedConstraint constraint) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.CONSTRAINT; + option.constraint = constraint; + return option; + } + + public IdentityDefinition getIdentityDefinition() { + return identityDefinition; + } + + public NamedConstraint getConstraint() { + return constraint; + } + + /** + * Returns the constraint kind explicitly represented by this option, including REFERENCES and + * NOT NULL. Ordinary DEFAULT options remain column options; an explicit DefaultConstraint node + * has kind DEFAULT. Returns OTHER for NULL, unrelated or raw options. No implicit constraints, + * such as NOT NULL implied by a primary key, or catalogue objects are inferred. + */ + public ConstraintKind getConstraintKind() { + switch (kind) { + case CONSTRAINT: + return constraint != null && constraint.getKind() != null ? constraint.getKind() + : ConstraintKind.OTHER; + case REFERENCE: + return foreignKeyReference == null ? ConstraintKind.OTHER + : ConstraintKind.FOREIGN_KEY; + case NULLABILITY: + return Boolean.FALSE.equals(nullable) ? ConstraintKind.NOT_NULL + : ConstraintKind.OTHER; + default: + return ConstraintKind.OTHER; + } + } + + /** Creates an opaque option with its own copy of the supplied tokens, without parsing them. */ + public static ColumnOption raw(List tokens) { + ColumnOption option = new ColumnOption(); + option.setTokens(tokens); + return option; + } + + public static ColumnOption raw(String... tokens) { + return raw(Arrays.asList(tokens)); + } + + public static ColumnOption serialDefaultValue() { + ColumnOption option = raw("SERIAL", "DEFAULT", "VALUE"); + option.kind = Kind.SERIAL_DEFAULT_VALUE; + return option; + } + + public static ColumnOption reference(ForeignKeyReference reference) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.REFERENCE; + option.foreignKeyReference = reference; + return option; + } + + public Kind getKind() { + return kind; + } + + /** + * Returns an unmodifiable, detached token snapshot for both raw and structured options. An + * unset raw option returns null; an explicitly empty raw option returns an empty snapshot. Use + * {@link #setTokens(List)} to edit an opaque option, or the typed setters and mutable child + * nodes to edit structured options. Later changes to the option do not change this snapshot. + */ + public List getTokens() { + switch (kind) { + case DEFAULT: + return Collections.unmodifiableList( + Arrays.asList("DEFAULT", String.valueOf(defaultExpression))); + case NULLABILITY: + return nullable ? Collections.singletonList("NULL") + : Collections.unmodifiableList(Arrays.asList("NOT", "NULL")); + case COLLATE: + return Collections.unmodifiableList(Arrays.asList("COLLATE", collation)); + case COMMENT: + return Collections.unmodifiableList(Arrays.asList("COMMENT", comment.toString())); + case ON_UPDATE: + return Collections.unmodifiableList( + Arrays.asList("ON", "UPDATE", onUpdateExpression.toString())); + case GENERATED: + return generatedDefinition.getTokens(); + case CONSTRAINT: + return constraintTokens(); + case SRID: + return Collections.unmodifiableList(Arrays.asList("SRID", String.valueOf(srid))); + case COLUMN_FORMAT: + return Collections.unmodifiableList( + Arrays.asList("COLUMN_FORMAT", columnFormat.name())); + case ENGINE_ATTRIBUTE: + case SECONDARY_ENGINE_ATTRIBUTE: + return Collections.unmodifiableList(useEquals + ? Arrays.asList(kind.name(), "=", engineAttribute.toString()) + : Arrays.asList(kind.name(), engineAttribute.toString())); + case OTHER: + case SERIAL_DEFAULT_VALUE: + return tokens == null ? null + : Collections.unmodifiableList(new ArrayList<>(tokens)); + default: + return Collections.singletonList(toString()); + } + } + + private List constraintTokens() { + String sql = constraint.toString(); + if (KEY_SPELLINGS.contains(sql)) { + return Collections.unmodifiableList(Arrays.asList(sql.split(" "))); + } + return Collections.singletonList(sql); + } + + /** + * Replaces the tokens of an opaque {@link Kind#OTHER} option with a copy of the supplied list. + * Null unsets the tokens; an empty list retains an explicitly empty option. Tokens are never + * parsed or classified. To replace a structured option, including SERIAL DEFAULT VALUE, with + * raw text, replace its node in {@link ColumnDefinition#getColumnOptions()} using + * {@link #raw(List)} instead. + * + * @throws IllegalStateException if this is a structured option + */ + public void setTokens(List tokens) { + if (kind != Kind.OTHER) { + throw new IllegalStateException( + "Only opaque options accept tokens; use typed setters or replace the option"); + } + this.tokens = tokens == null ? null : new ArrayList<>(tokens); + } + + public ForeignKeyReference getForeignKeyReference() { + return foreignKeyReference; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } + + /** Appends the option using the supplied printer for structured expressions. */ + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + switch (kind) { + case FOREIGN_OPTIONS: + ForeignDataOption.appendOptionsTo(builder, foreignOptions, expressionPrinter); + break; + case STORAGE: + builder.append("STORAGE ").append(storage); + break; + case COMPRESSION: + builder.append("COMPRESSION ").append(compression); + break; + case SRID: + builder.append("SRID ").append(srid); + break; + case COLUMN_FORMAT: + builder.append("COLUMN_FORMAT ").append(columnFormat); + break; + case ENGINE_ATTRIBUTE: + case SECONDARY_ENGINE_ATTRIBUTE: + builder.append(kind).append(useEquals ? " = " : " "); + expressionPrinter.accept(engineAttribute); + break; + case DEFAULT: + builder.append("DEFAULT "); + expressionPrinter.accept(defaultExpression); + break; + case NULLABILITY: + builder.append(nullable ? "NULL" : "NOT NULL"); + break; + case VISIBILITY: + builder.append(visible ? "VISIBLE" : "INVISIBLE"); + break; + case AUTO_INCREMENT: + builder.append("AUTO_INCREMENT"); + break; + case COLLATE: + builder.append("COLLATE ").append(collation); + break; + case COMMENT: + builder.append("COMMENT "); + expressionPrinter.accept(comment); + break; + case ON_UPDATE: + builder.append("ON UPDATE "); + expressionPrinter.accept(onUpdateExpression); + break; + case GENERATED: + generatedDefinition.appendTo(builder, expressionPrinter); + break; + case REFERENCE: + builder.append(foreignKeyReference); + break; + case IDENTITY: + builder.append(identityDefinition); + break; + case CONSTRAINT: + constraint.appendTo(builder, expressionPrinter); + break; + default: + builder.append(PlainSelect.getStringList(tokens, false, false)); + break; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintAttributes.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintAttributes.java new file mode 100644 index 0000000000..7f8b68bd02 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintAttributes.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.statement.create.table; + +import java.io.Serializable; + +/** Shared PostgreSQL constraint attributes; null values preserve omitted clauses. */ +public class ConstraintAttributes implements Serializable { + public enum Initially { + IMMEDIATE, DEFERRED + } + + private Boolean deferrable; + private Initially initially; + private boolean notValid; + private Boolean enforced; + + public Boolean getDeferrable() { + return deferrable; + } + + public void setDeferrable(Boolean deferrable) { + this.deferrable = deferrable; + } + + public Initially getInitially() { + return initially; + } + + public void setInitially(Initially initially) { + this.initially = initially; + } + + public boolean isNotValid() { + return notValid; + } + + public void setNotValid(boolean notValid) { + this.notValid = notValid; + } + + /** Null preserves an omitted ENFORCED clause. */ + public Boolean getEnforced() { + return enforced; + } + + public void setEnforced(Boolean enforced) { + this.enforced = enforced; + } + + public void appendTo(StringBuilder sql) { + if (deferrable != null) { + sql.append(deferrable ? " DEFERRABLE" : " NOT DEFERRABLE"); + } + if (initially != null) { + sql.append(" INITIALLY ").append(initially); + } + if (enforced != null) { + sql.append(enforced ? " ENFORCED" : " NOT ENFORCED"); + } + if (notValid) { + sql.append(" NOT VALID"); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql); + return sql.toString().trim(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintDeclaration.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintDeclaration.java new file mode 100644 index 0000000000..b76ccf3a4d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintDeclaration.java @@ -0,0 +1,64 @@ +/*- + * #%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.statement.create.table; + +/** + * A view of a constraint declared in a CREATE TABLE definition. Its source nodes remain editable; + * no constraint data is copied. A column declaration retains its owning column rather than + * manufacturing a table constraint or a key-column list. + * + * @see CreateTable#getConstraints() + */ +public final class ConstraintDeclaration { + private final ColumnDefinition column; + private final ColumnOption columnOption; + private final NamedConstraint constraint; + + ConstraintDeclaration(ColumnDefinition column, ColumnOption columnOption) { + this.column = column; + this.columnOption = columnOption; + this.constraint = null; + } + + ConstraintDeclaration(NamedConstraint constraint) { + this.column = null; + this.columnOption = null; + this.constraint = constraint; + } + + /** + * Returns the current kind, or OTHER if it is unknown or an option no longer declares a + * constraint. + */ + public ConstraintKind getKind() { + if (columnOption != null) { + return columnOption.getConstraintKind(); + } + return constraint.getKind() != null ? constraint.getKind() : ConstraintKind.OTHER; + } + + /** Returns the original owning column, or null for a table-level declaration. */ + public ColumnDefinition getColumn() { + return column; + } + + /** Returns the original column option, or null for a table-level declaration. */ + public ColumnOption getColumnOption() { + return columnOption; + } + + /** + * Returns the original constraint node, including a column's nested constraint. REFERENCES and + * nullability column options have no NamedConstraint node; use getColumnOption() for them. + */ + public NamedConstraint getConstraint() { + return columnOption == null ? constraint : columnOption.getConstraint(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintKind.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintKind.java new file mode 100644 index 0000000000..1ea9e55052 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintKind.java @@ -0,0 +1,40 @@ +/*- + * #%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.statement.create.table; + +/** The kind of a declared table or column constraint, independent of its supporting index. */ +public enum ConstraintKind { + PRIMARY_KEY, UNIQUE, FOREIGN_KEY, CHECK, EXCLUDE, DEFAULT, NOT_NULL, OTHER; + + public static ConstraintKind fromType(String type) { + if (type == null) { + return OTHER; + } + String normalized = type.trim().toUpperCase(java.util.Locale.ROOT); + switch (normalized.split("\\s+", 2)[0]) { + case "PRIMARY": + return PRIMARY_KEY; + case "UNIQUE": + return UNIQUE; + case "FOREIGN": + return FOREIGN_KEY; + case "CHECK": + return CHECK; + case "EXCLUDE": + return EXCLUDE; + case "DEFAULT": + return DEFAULT; + case "NOT": + return "NOT NULL".equals(normalized) ? NOT_NULL : OTHER; + default: + return OTHER; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintUsingIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintUsingIndex.java new file mode 100644 index 0000000000..f7f1798b8a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintUsingIndex.java @@ -0,0 +1,63 @@ +/*- + * #%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.statement.create.table; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** + * A PostgreSQL UNIQUE or PRIMARY KEY constraint backed by an existing index. The referenced index + * is distinct from the constraint name and from a newly declared index. + */ +public class ConstraintUsingIndex extends NamedConstraint { + private String existingIndexName; + + public ConstraintUsingIndex() { + setType("UNIQUE"); + } + + /** Returns the referenced index identifier, retaining SQL quotes. */ + public String getExistingIndexName() { + return existingIndexName; + } + + public void setExistingIndexName(String existingIndexName) { + this.existingIndexName = existingIndexName; + } + + public ConstraintUsingIndex withExistingIndexName(String existingIndexName) { + setExistingIndexName(existingIndexName); + return this; + } + + @Override + public ConstraintUsingIndex withName(String name) { + super.withName(name); + return this; + } + + @Override + public ConstraintUsingIndex withType(String type) { + super.withType(type); + return this; + } + + public ConstraintUsingIndex withConstraintAttributes(ConstraintAttributes attributes) { + setConstraintAttributes(attributes); + return this; + } + + @Override + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { + appendConstraintPrefixTo(sql); + sql.append(getType()).append(" USING INDEX ").append(existingIndexName); + appendConstraintAttributesTo(sql); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java index d2be0e0264..ed0d930097 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java @@ -16,31 +16,91 @@ import java.util.Optional; import net.sf.jsqlparser.expression.SpannerInterleaveIn; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.LikeClause; import net.sf.jsqlparser.statement.StatementVisitor; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; -public class CreateTable implements Statement { +public class CreateTable extends ASTNodeAccessImpl implements Statement { + private boolean tableOptionsAfterPartition; + private net.sf.jsqlparser.statement.execute.Execute execute; private Table table; private boolean unlogged = false; private List createOptionsStrings; private List tableOptionsStrings; + private List tableOptions; private List columnDefinitions; private List columns; private List indexes; + private List tableConstraints; + private List tableElements; private Select select; + private DuplicateHandling duplicateHandling; private Table likeTable; + private Table cloneTable; + private List
inherits; + private ColDataType ofType; private boolean selectParenthesis; + private boolean useAsKeyword = true; + private Boolean withData; private boolean ifNotExists = false; private boolean orReplace = false; + private TablePartitioning partitioning; + private Table partitionOf; + private PartitionBound partitionBound; private RowMovement rowMovement; private SpannerInterleaveIn interleaveIn = null; + public enum DuplicateHandling { + IGNORE, REPLACE + } + + public boolean isTableOptionsAfterPartition() { + return tableOptionsAfterPartition; + } + + public void setTableOptionsAfterPartition(boolean tableOptionsAfterPartition) { + this.tableOptionsAfterPartition = tableOptionsAfterPartition; + } + + /** Prepared statement source of PostgreSQL CREATE TABLE AS EXECUTE. */ + public net.sf.jsqlparser.statement.execute.Execute getExecute() { + return execute; + } + + public void setExecute(net.sf.jsqlparser.statement.execute.Execute execute) { + this.execute = execute; + select = null; + selectParenthesis = false; + } + + /** The structured SERVER clause, also present in the shared table-options list. */ + public ForeignTableOptions getForeignTableOptions() { + if (tableOptions != null) { + for (TableOption option : tableOptions) { + if (option.getForeignTableOptions() != null) { + return option.getForeignTableOptions(); + } + } + } + return null; + } + + /** MySQL's duplicate-key handling when creating a table from a query; null if omitted. */ + public DuplicateHandling getDuplicateHandling() { + return duplicateHandling; + } + + public void setDuplicateHandling(DuplicateHandling duplicateHandling) { + this.duplicateHandling = duplicateHandling; + } + @Override public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); @@ -63,14 +123,19 @@ public void setUnlogged(boolean unlogged) { } /** - * @return a list of {@link ColumnDefinition}s of this table. + * @return a list of {@link ColumnDefinition}s of this table. When ordered table elements are + * present, this is a mutable view of the column definitions in that list. */ public List getColumnDefinitions() { return columnDefinitions; } public void setColumnDefinitions(List list) { - columnDefinitions = list; + if (tableElements == null) { + columnDefinitions = list; + } else { + TableElementList.replace(tableElements, ColumnDefinition.class, list); + } } public List getColumns() { @@ -83,14 +148,41 @@ public void setColumns(List columns) { /** * @return a list of options (as simple strings) of this table definition, as ("TYPE", "=", - * "MYISAM") + * "MYISAM"). For typed options, this is a snapshot of their current tokens; use + * {@link #getTableOptions()} to edit the structured options or + * {@link #setTableOptionsStrings(List)} to replace them with raw options. */ public List getTableOptionsStrings() { - return tableOptionsStrings; + if (tableOptions == null) { + return tableOptionsStrings; + } + List tokens = new ArrayList<>(); + for (TableOption option : tableOptions) { + tokens.addAll(option.getTokens()); + } + return tokens; } public void setTableOptionsStrings(List tableOptionsStrings) { this.tableOptionsStrings = tableOptionsStrings; + tableOptions = null; + } + + /** Returns typed table options in source order. */ + public List getTableOptions() { + return tableOptions; + } + + public void setTableOptions(List tableOptions) { + this.tableOptions = tableOptions; + tableOptionsStrings = null; + } + + /** Returns the first option of the requested kind, if present. */ + public Optional getTableOption(TableOption.Kind kind) { + return Optional.ofNullable(tableOptions).orElseGet(Collections::emptyList).stream() + .filter(option -> option.getKind() == kind) + .findFirst(); } public List getCreateOptionsStrings() { @@ -102,36 +194,230 @@ public void setCreateOptionsStrings(List createOptionsStrings) { } /** - * @return a list of {@link Index}es (for example "PRIMARY KEY") of this table.
- * Indexes created with column definitions (as in mycol INT PRIMARY KEY) are not - * inserted into this list. + * Returns index declarations only. Table constraints are available through + * {@link #getTableConstraints()}. When ordered table elements are present, this is a mutable + * view of their indexes. */ public List getIndexes() { return indexes; } + /** + * Returns column and table constraint declarations in source order, including MySQL UNIQUE + * KEY/INDEX declarations, REFERENCES and explicit NOT NULL options. Ordinary DEFAULT column + * options are not classified as constraints; explicit {@link DefaultConstraint} nodes are. + * Index declarations, raw options, and constraints inherited from LIKE, a type or another table + * are not inferred. + * + * The list is an unmodifiable membership snapshot. Each view reads its original editable AST + * nodes; call this method again after inserting or removing declarations or changing a + * nullability option between NULL and NOT NULL. Typed constraint nodes remain included even + * when their kind is OTHER. For legacy construction without ordered table elements, columns + * precede table constraints. + */ + public List getConstraints() { + List declarations = new ArrayList<>(); + if (tableElements != null) { + tableElements.forEach(element -> addConstraints(element, declarations)); + } else { + if (columnDefinitions != null) { + columnDefinitions.forEach(column -> addConstraints(column, declarations)); + } + if (tableConstraints != null) { + tableConstraints.forEach(constraint -> addConstraints(constraint, declarations)); + } + } + return Collections.unmodifiableList(declarations); + } + + private static void addConstraints(TableElement element, + List declarations) { + if (element instanceof ColumnDefinition) { + ColumnDefinition column = (ColumnDefinition) element; + if (column.getColumnOptions() != null) { + for (ColumnOption option : column.getColumnOptions()) { + if (option.getKind() == ColumnOption.Kind.CONSTRAINT + && option.getConstraint() != null + || option.getConstraintKind() != ConstraintKind.OTHER) { + declarations.add(new ConstraintDeclaration(column, option)); + } + } + } + } else if (element instanceof NamedConstraint) { + declarations.add(new ConstraintDeclaration((NamedConstraint) element)); + } + } + public void setIndexes(List list) { - indexes = list; + if (tableElements == null) { + indexes = list; + } else { + TableElementList.replace(tableElements, Index.class, list); + } + } + + /** + * Returns table-level constraint definitions, excluding constraints declared within columns. + * When ordered table elements are present, this is a mutable view of their constraints. Use + * {@link #getConstraints()} to inspect both column and table declarations. + */ + public List getTableConstraints() { + return tableConstraints; + } + + public void setTableConstraints(List constraints) { + if (tableElements == null) { + tableConstraints = constraints; + } else { + TableElementList.replace(tableElements, NamedConstraint.class, constraints); + } + } + + /** + * Returns columns, constraints, and indexes in the order in which they were declared. + */ + public List getTableElements() { + return tableElements; + } + + public void setTableElements(List tableElements) { + this.tableElements = tableElements; + if (tableElements == null) { + columnDefinitions = null; + indexes = null; + tableConstraints = null; + return; + } + columnDefinitions = new TableElementList<>(tableElements, ColumnDefinition.class); + indexes = new TableElementList<>(tableElements, Index.class); + tableConstraints = new TableElementList<>(tableElements, NamedConstraint.class); + } + + /** Returns table elements of a requested AST type while preserving their declaration order. */ + public List getTableElements(Class type) { + if (tableElements == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (TableElement element : tableElements) { + if (type.isInstance(element)) { + result.add(type.cast(element)); + } + } + return result; } public Select getSelect() { return select; } + public boolean isUseAsKeyword() { + return useAsKeyword; + } + + public void setUseAsKeyword(boolean useAsKeyword) { + this.useAsKeyword = useAsKeyword; + } + + /** Returns null for omission, true for WITH DATA, and false for WITH NO DATA. */ + public Boolean getWithData() { + return withData; + } + + public void setWithData(Boolean withData) { + this.withData = withData; + } + + public StringBuilder appendSelectTo(StringBuilder builder, + java.util.function.Consumer selectRenderer, + java.util.function.Consumer executeRenderer) { + if (select != null || execute != null) { + if (duplicateHandling != null) { + builder.append(' ').append(duplicateHandling); + } + builder.append(useAsKeyword ? " AS " : " "); + if (selectParenthesis) { + builder.append("("); + } + if (execute != null) { + executeRenderer.accept(execute); + } else { + selectRenderer.accept(select); + } + if (selectParenthesis) { + builder.append(")"); + } + if (withData != null) { + builder.append(withData ? " WITH DATA" : " WITH NO DATA"); + } + } + return builder; + } + public void setSelect(Select select, boolean parenthesis) { this.select = select; + this.execute = null; this.selectParenthesis = parenthesis; } public Table getLikeTable() { + if (likeTable != null) { + return likeTable; + } + List clauses = getTableElements(LikeClause.class); + return clauses.isEmpty() ? null : clauses.get(0).getTable(); + } + + /** Returns the legacy trailing LIKE source, excluding LIKE clauses inside the definition. */ + public Table getTrailingLikeTable() { return likeTable; } + public ColDataType getOfType() { + return ofType; + } + + public void setOfType(ColDataType ofType) { + this.ofType = ofType; + } + public void setLikeTable(Table likeTable, boolean parenthesis) { + List clauses = getTableElements(LikeClause.class); + if (this.likeTable == null && !clauses.isEmpty()) { + if (likeTable != null) { + clauses.get(0).setTable(likeTable); + } else { + List elements = new ArrayList<>(tableElements); + elements.remove(clauses.get(0)); + setTableElements(elements); + } + return; + } this.likeTable = likeTable; this.selectParenthesis = parenthesis; } + /** + * BigQuery's {@code CREATE [SNAPSHOT] TABLE target CLONE source [FOR SYSTEM_TIME AS OF ...]}. + */ + public Table getCloneTable() { + return cloneTable; + } + + public void setCloneTable(Table cloneTable) { + this.cloneTable = cloneTable; + } + + public CreateTable withCloneTable(Table cloneTable) { + setCloneTable(cloneTable); + return this; + } + public boolean isIfNotExists() { return ifNotExists; } @@ -148,6 +434,51 @@ public void setOrReplace(boolean orReplace) { this.orReplace = orReplace; } + public TablePartitioning getPartitioning() { + return partitioning; + } + + public void setPartitioning(TablePartitioning partitioning) { + this.partitioning = partitioning; + } + + public Table getPartitionOf() { + return partitionOf; + } + + public void setPartitionOf(Table partitionOf) { + this.partitionOf = partitionOf; + } + + public PartitionBound getPartitionBound() { + return partitionBound; + } + + public void setPartitionBound(PartitionBound partitionBound) { + this.partitionBound = partitionBound; + } + + /** PostgreSQL parent tables, in declaration order; null if INHERITS is absent. */ + public List
getInherits() { + return inherits; + } + + public void setInherits(List
inherits) { + this.inherits = inherits; + } + + public CreateTable withInherits(List
inherits) { + setInherits(inherits); + return this; + } + + /** Shared rendering of the structured parent references. */ + public void appendInheritanceTo(StringBuilder builder) { + if (inherits != null) { + builder.append(" INHERITS ").append(PlainSelect.getStringList(inherits, true, true)); + } + } + public boolean isSelectParenthesis() { return selectParenthesis; } @@ -170,6 +501,10 @@ public String toString() { StringBuilder b = new StringBuilder(); appendCreateClause(b); appendColumnDefinitions(b); + appendInheritanceTo(b); + if (partitionBound != null) { + b.append(" ").append(partitionBound); + } appendTableOptions(b); appendTableProperties(b); return b.toString(); @@ -193,6 +528,15 @@ private void appendCreateClause(StringBuilder b) { b.append("IF NOT EXISTS "); } b.append(table); + if (ofType != null) { + b.append(" OF ").append(ofType); + } + if (partitionOf != null) { + b.append(" PARTITION OF ").append(partitionOf); + } + if (cloneTable != null) { + b.append(" CLONE ").append(cloneTable); + } } private void appendColumnDefinitions(StringBuilder b) { @@ -200,21 +544,51 @@ private void appendColumnDefinitions(StringBuilder b) { b.append(" "); b.append(PlainSelect.getStringList(columns, true, true)); } - if (columnDefinitions != null && !columnDefinitions.isEmpty()) { + if (tableElements != null) { b.append(" ("); - b.append(PlainSelect.getStringList(columnDefinitions, true, false)); - if (indexes != null && !indexes.isEmpty()) { - b.append(", "); - b.append(PlainSelect.getStringList(indexes)); - } + b.append(PlainSelect.getStringList(tableElements, true, false)); b.append(")"); + } else { + List definitions = new ArrayList<>(); + if (columnDefinitions != null) { + definitions.addAll(columnDefinitions); + } + if (tableConstraints != null) { + definitions.addAll(tableConstraints); + } + if (indexes != null) { + definitions.addAll(indexes); + } + if (!definitions.isEmpty()) { + b.append(" ("); + b.append(PlainSelect.getStringList(definitions, true, false)); + b.append(")"); + } + } + } + + public void appendTableOptionsTo(StringBuilder builder, + java.util.function.Consumer expressionPrinter) { + if (tableOptions != null) { + for (TableOption option : tableOptions) { + builder.append(' '); + option.appendTo(builder, expressionPrinter); + } + } else { + String options = PlainSelect.getStringList(tableOptionsStrings, false, false); + if (options != null && !options.isEmpty()) { + builder.append(' ').append(options); + } } } private void appendTableOptions(StringBuilder b) { - String options = PlainSelect.getStringList(tableOptionsStrings, false, false); - if (options != null && options.length() > 0) { - b.append(" ").append(options); + if (partitioning != null && tableOptionsAfterPartition) { + b.append(" ").append(partitioning); + } + appendTableOptionsTo(b, b::append); + if (partitioning != null && !tableOptionsAfterPartition) { + b.append(" ").append(partitioning); } } @@ -222,16 +596,7 @@ private void appendTableProperties(StringBuilder b) { if (rowMovement != null) { b.append(" ").append(rowMovement.getMode()).append(" ROW MOVEMENT"); } - if (select != null) { - b.append(" AS "); - if (selectParenthesis) { - b.append("("); - } - b.append(select); - if (selectParenthesis) { - b.append(")"); - } - } + appendSelectTo(b, b::append); if (likeTable != null) { b.append(" LIKE "); if (selectParenthesis) { @@ -272,6 +637,21 @@ public CreateTable withIfNotExists(boolean ifNotExists) { return this; } + public CreateTable withPartitioning(TablePartitioning partitioning) { + this.setPartitioning(partitioning); + return this; + } + + public CreateTable withPartitionOf(Table partitionOf) { + setPartitionOf(partitionOf); + return this; + } + + public CreateTable withPartitionBound(PartitionBound partitionBound) { + setPartitionBound(partitionBound); + return this; + } + public CreateTable withRowMovement(RowMovement rowMovement) { this.setRowMovement(rowMovement); return this; @@ -282,6 +662,11 @@ public CreateTable withTableOptionsStrings(List tableOptionsStrings) { return this; } + public CreateTable withTableOptions(List tableOptions) { + this.setTableOptions(tableOptions); + return this; + } + public CreateTable withColumnDefinitions(List columnDefinitions) { this.setColumnDefinitions(columnDefinitions); return this; @@ -297,6 +682,16 @@ public CreateTable withIndexes(List indexes) { return this; } + public CreateTable withTableConstraints(List constraints) { + setTableConstraints(constraints); + return this; + } + + public CreateTable withTableElements(List tableElements) { + this.setTableElements(tableElements); + return this; + } + public CreateTable addCreateOptionsStrings(String... createOptionsStrings) { List collection = Optional.ofNullable(getCreateOptionsStrings()).orElseGet(ArrayList::new); @@ -350,6 +745,20 @@ public CreateTable addIndexes(Collection indexes) { return this.withIndexes(collection); } + public CreateTable addTableConstraints(NamedConstraint... constraints) { + List collection = + Optional.ofNullable(getTableConstraints()).orElseGet(ArrayList::new); + Collections.addAll(collection, constraints); + return withTableConstraints(collection); + } + + public CreateTable addTableConstraints(Collection constraints) { + List collection = + Optional.ofNullable(getTableConstraints()).orElseGet(ArrayList::new); + collection.addAll(constraints); + return withTableConstraints(collection); + } + public SpannerInterleaveIn getSpannerInterleaveIn() { return interleaveIn; } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/DefaultConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/DefaultConstraint.java new file mode 100644 index 0000000000..4c123351e5 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/DefaultConstraint.java @@ -0,0 +1,100 @@ +/*- + * #%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.statement.create.table; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; + +/** A SQL Server table-level DEFAULT expression FOR a column, with an optional constraint name. */ +public class DefaultConstraint extends NamedConstraint { + private Expression expression; + private Column column; + private boolean withValues; + + public DefaultConstraint() { + setType("DEFAULT"); + + } + + @Override + public ConstraintKind getKind() { + return ConstraintKind.DEFAULT; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public Column getColumn() { + return column; + } + + public void setColumn(Column column) { + this.column = column; + } + + public boolean isWithValues() { + return withValues; + } + + public void setWithValues(boolean withValues) { + this.withValues = withValues; + } + + public DefaultConstraint withExpression(Expression expression) { + setExpression(expression); + return this; + } + + public DefaultConstraint withColumn(Column column) { + setColumn(column); + return this; + } + + public DefaultConstraint withWithValues(boolean withValues) { + setWithValues(withValues); + return this; + } + + @Override + public DefaultConstraint withName(String name) { + setName(name); + return this; + } + + /** Shares rendering with deparsers while allowing both the value and column to be visited. */ + public void appendTo(StringBuilder builder, Consumer expressionWriter) { + if (expression == null || column == null) { + throw new IllegalStateException("DEFAULT requires an expression and a target column"); + } + if (getName() != null) { + builder.append("CONSTRAINT ").append(getName()).append(' '); + } + builder.append("DEFAULT "); + expressionWriter.accept(expression); + builder.append(" FOR "); + expressionWriter.accept(column); + if (withValues) { + builder.append(" WITH VALUES"); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ExcludeConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ExcludeConstraint.java index 4cdc37bc4c..d4306ff024 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ExcludeConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ExcludeConstraint.java @@ -9,14 +9,30 @@ */ package net.sf.jsqlparser.statement.create.table; +import java.util.ArrayList; import java.util.Collection; import java.util.List; - +import java.util.Objects; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; -public class ExcludeConstraint extends Index { - +/** + * An exclusion constraint with key elements, supporting-index options and an optional predicate. + */ +public class ExcludeConstraint extends NamedConstraint + implements KeyColumnSource, IndexOptionSource { private Expression expression; + private List columns; + private IndexOptions indexOptions = new IndexOptions(); + + public ExcludeConstraint() { + setType("EXCLUDE"); + } + + @Override + public ConstraintKind getKind() { + return ConstraintKind.EXCLUDE; + } public Expression getExpression() { return expression; @@ -26,72 +42,113 @@ public void setExpression(Expression expression) { this.expression = expression; } - @Override - public String toString() { - StringBuilder exclusionStatement = new StringBuilder("EXCLUDE WHERE "); - exclusionStatement.append("("); - exclusionStatement.append(expression); - exclusionStatement.append(")"); - return exclusionStatement.toString(); + public E getExpression(Class type) { + return type.cast(expression); } public ExcludeConstraint withExpression(Expression expression) { - this.setExpression(expression); + setExpression(expression); return this; } - public E getExpression(Class type) { - return type.cast(getExpression()); + @Override + public List getColumns() { + return columns; } @Override - public ExcludeConstraint withName(List name) { - return (ExcludeConstraint) super.withName(name); + public void setColumns(List columns) { + this.columns = columns; } @Override - public ExcludeConstraint withType(String type) { - return (ExcludeConstraint) super.withType(type); + public IndexOptions getIndexOptions() { + return indexOptions; + } + + public void setIndexOptions(IndexOptions options) { + indexOptions = Objects.requireNonNull(options, "indexOptions"); + } + + public ExcludeConstraint withColumns(List columns) { + setColumns(columns); + return this; + } + + public ExcludeConstraint withColumnsNames(List columns) { + setColumnsNames(columns); + return this; + } + + public ExcludeConstraint addColumns(KeyElement... values) { + return addColumns(java.util.Arrays.asList(values)); + } + + public ExcludeConstraint addColumns(Collection values) { + if (columns == null) { + columns = new ArrayList<>(); + } + columns.addAll(values); + return this; } - @Override public ExcludeConstraint withUsing(String using) { - return (ExcludeConstraint) super.withUsing(using); + setUsing(using); + return this; } - @Override - public ExcludeConstraint withColumnsNames(List list) { - return (ExcludeConstraint) super.withColumnsNames(list); + public ExcludeConstraint withIndexSpec(List options) { + setIndexSpec(options); + return this; } - @Override - public ExcludeConstraint withColumns(List columns) { - return (ExcludeConstraint) super.withColumns(columns); + public ExcludeConstraint withClustering(IndexOptions.Clustering clustering) { + setClustering(clustering); + return this; } @Override - public ExcludeConstraint addColumns(ColumnParams... functionDeclarationParts) { - return (ExcludeConstraint) super.addColumns(functionDeclarationParts); + public ExcludeConstraint withType(String type) { + setType(type); + return this; } @Override - public ExcludeConstraint addColumns( - Collection functionDeclarationParts) { - return (ExcludeConstraint) super.addColumns(functionDeclarationParts); + public ExcludeConstraint withName(String name) { + setName(name); + return this; } @Override - public ExcludeConstraint withIndexSpec(List idxSpec) { - return (ExcludeConstraint) super.withIndexSpec(idxSpec); + public ExcludeConstraint withName(List name) { + setName(name); + return this; } @Override - public ExcludeConstraint withName(String name) { - return (ExcludeConstraint) super.withName(name); + public ExcludeConstraint withUseConstraintKeyword(boolean value) { + setUseConstraintKeyword(value); + return this; } @Override - public ExcludeConstraint withIndexKeyword(String indexKeyword) { - return (ExcludeConstraint) super.withIndexKeyword(indexKeyword); + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { + appendConstraintPrefixTo(sql); + sql.append("EXCLUDE"); + if (getUsing() != null) { + sql.append(" USING ").append(getUsing()); + } + if (columns != null) { + sql.append(' '); + appendColumnsTo(sql, expressionPrinter); + } + appendIndexOptionsTo(sql, expressionPrinter); + if (expression != null) { + sql.append(" WHERE ("); + expressionPrinter.accept(expression); + sql.append(')'); + } + appendConstraintSuffixTo(sql); + appendConstraintAttributesTo(sql); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java new file mode 100644 index 0000000000..182b1e124b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java @@ -0,0 +1,71 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; + +/** + * An exclusion operator, optionally qualified and enclosed in PostgreSQL OPERATOR(...). COMMENT ON + * OPERATOR uses the same reference without the OPERATOR keyword. + */ +public class ExclusionOperator implements Serializable { + private String schemaName; + private String name; + private boolean useOperatorKeyword; + + public String getSchemaName() { + return schemaName; + } + + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isUseOperatorKeyword() { + return useOperatorKeyword; + } + + public void setUseOperatorKeyword(boolean useOperatorKeyword) { + this.useOperatorKeyword = useOperatorKeyword; + } + + public ExclusionOperator withSchemaName(String schemaName) { + setSchemaName(schemaName); + return this; + } + + public ExclusionOperator withName(String name) { + setName(name); + return this; + } + + public ExclusionOperator withUseOperatorKeyword(boolean useOperatorKeyword) { + setUseOperatorKeyword(useOperatorKeyword); + return this; + } + + /** Returns the qualified name without an OPERATOR wrapper, preserving identifier quotes. */ + public String getQualifiedName() { + return (schemaName == null ? "" : schemaName + ".") + name; + } + + @Override + public String toString() { + return useOperatorKeyword ? "OPERATOR(" + getQualifiedName() + ")" : getQualifiedName(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignDataOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignDataOption.java new file mode 100644 index 0000000000..9bc11c1a1b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignDataOption.java @@ -0,0 +1,100 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; + +/** A foreign table or column option; absent action means the implicit ADD form. */ +public class ForeignDataOption implements Serializable { + public enum Action { + ADD, SET, DROP + } + + private Action action; + private String name; + private StringValue value; + + public ForeignDataOption(String name, StringValue value) { + this.name = name; + this.value = value; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + if (action == Action.DROP) { + value = null; + } + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public StringValue getValue() { + return value; + } + + public void setValue(StringValue value) { + this.value = value; + } + + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + if (action != null) { + builder.append(action).append(' '); + } + builder.append(name); + if (action != Action.DROP && value != null) { + builder.append(' '); + expressionPrinter.accept(value); + } + } + + public static void appendOptionsTo(StringBuilder builder, List options, + Consumer expressionPrinter) { + builder.append("OPTIONS ("); + for (int i = 0; i < options.size(); i++) { + if (i > 0) { + builder.append(", "); + } + options.get(i).appendTo(builder, expressionPrinter); + } + builder.append(')'); + } + + public static void visitExpressions(List options, + Consumer visitor) { + if (options != null) { + for (ForeignDataOption option : options) { + if (option.action != Action.DROP && option.value != null) { + visitor.accept(option.value); + } + } + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyConstraint.java new file mode 100644 index 0000000000..2c0254917f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyConstraint.java @@ -0,0 +1,259 @@ +/*- + * #%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.statement.create.table; + +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.ReferentialAction; +import net.sf.jsqlparser.statement.ReferentialAction.Action; +import net.sf.jsqlparser.statement.ReferentialAction.Type; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +public class ForeignKeyConstraint extends NamedConstraint implements KeyColumnSource { + private List columns; + private String indexName; + private ForeignKeyReference reference = new ForeignKeyReference(); + + public ForeignKeyConstraint() { + setType("FOREIGN KEY"); + } + + @Override + public ConstraintKind getKind() { + return ConstraintKind.FOREIGN_KEY; + } + + @Override + public List getColumns() { + return columns; + } + + @Override + public void setColumns(List columns) { + this.columns = columns; + } + + public String getIndexName() { + return indexName; + } + + public void setIndexName(String value) { + indexName = value; + } + + /** Returns the mutable reference shared by the structured and legacy accessors. */ + public ForeignKeyReference getReference() { + return reference; + } + + /** + * Replaces the reference. A null value detaches its current table, referenced columns and + * referential actions from reference-only options such as MATCH, preserving the legacy setter + * contract without restoring stale values from before structured edits. + */ + public void setReference(ForeignKeyReference reference) { + if (reference == null) { + ForeignKeyReference detached = new ForeignKeyReference() + .withTable(this.reference.getTable()) + .withReferencedColumnNames(this.reference.getReferencedColumnNames()); + detached.getReferentialActions().addAll(this.reference.getReferentialActions()); + this.reference = detached; + } else { + this.reference = reference; + } + } + + public ForeignKeyReference.MatchType getMatchType() { + return reference.getMatchType(); + } + + public void setMatchType(ForeignKeyReference.MatchType matchType) { + getReference().setMatchType(matchType); + } + + public Table getTable() { + return reference.getTable(); + } + + public void setTable(Table table) { + reference.setTable(table); + } + + public List getReferencedColumnNames() { + return reference.getReferencedColumnNames(); + } + + public void setReferencedColumnNames(List referencedColumnNames) { + reference.setReferencedColumnNames(referencedColumnNames); + } + + /** + * @param type + * @param action + */ + public void setReferentialAction(Type type, Action action) { + reference.setReferentialAction(type, action); + } + + public ForeignKeyConstraint withReferentialAction(Type type, Action action) { + setReferentialAction(type, action); + return this; + } + + /** + * @param type + */ + public void removeReferentialAction(Type type) { + reference.removeReferentialAction(type); + } + + /** + * @param type + * @return + */ + public ReferentialAction getReferentialAction(Type type) { + return reference.getReferentialAction(type); + } + + @Deprecated + public String getOnDeleteReferenceOption() { + ReferentialAction a = getReferentialAction(Type.DELETE); + return a == null ? null : a.getAction().getAction(); + } + + @Deprecated + public void setOnDeleteReferenceOption(String onDeleteReferenceOption) { + if (onDeleteReferenceOption == null) { + removeReferentialAction(Type.DELETE); + } else { + setReferentialAction(Type.DELETE, Action.from(onDeleteReferenceOption)); + } + } + + @Deprecated + public String getOnUpdateReferenceOption() { + ReferentialAction a = getReferentialAction(Type.UPDATE); + return a == null ? null : a.getAction().getAction(); + } + + @Deprecated + public void setOnUpdateReferenceOption(String onUpdateReferenceOption) { + if (onUpdateReferenceOption == null) { + removeReferentialAction(Type.UPDATE); + } else { + setReferentialAction(Type.UPDATE, Action.from(onUpdateReferenceOption)); + } + } + + @Override + public void appendTo(StringBuilder b, Consumer expressionPrinter) { + appendConstraintPrefixTo(b); + b.append(getType()); + if (indexName != null) { + b.append(' ').append(indexName); + } + if (columns != null) { + b.append(' '); + appendColumnsTo(b, expressionPrinter); + } + b.append(' ').append(reference); + appendConstraintSuffixTo(b); + appendConstraintAttributesTo(b); + } + + public ForeignKeyConstraint withTable(Table table) { + this.setTable(table); + return this; + } + + public ForeignKeyConstraint withReferencedColumnNames(List referencedColumnNames) { + this.setReferencedColumnNames(referencedColumnNames); + return this; + } + + public ForeignKeyConstraint withReference(ForeignKeyReference reference) { + setReference(reference); + return this; + } + + public ForeignKeyConstraint withMatchType(ForeignKeyReference.MatchType matchType) { + setMatchType(matchType); + return this; + } + + public ForeignKeyConstraint withOnDeleteReferenceOption(String onDeleteReferenceOption) { + this.setOnDeleteReferenceOption(onDeleteReferenceOption); + return this; + } + + public ForeignKeyConstraint withOnUpdateReferenceOption(String onUpdateReferenceOption) { + this.setOnUpdateReferenceOption(onUpdateReferenceOption); + return this; + } + + public ForeignKeyConstraint addReferencedColumnNames(String... referencedColumnNames) { + reference.addReferencedColumnNames(referencedColumnNames); + return this; + } + + public ForeignKeyConstraint addReferencedColumnNames(Collection referencedColumnNames) { + reference.addReferencedColumnNames(referencedColumnNames); + return this; + } + + @Override + public ForeignKeyConstraint withType(String type) { + return (ForeignKeyConstraint) super.withType(type); + } + + @Override + public ForeignKeyConstraint withUseConstraintKeyword(boolean useConstraintKeyword) { + return (ForeignKeyConstraint) super.withUseConstraintKeyword(useConstraintKeyword); + } + + @Override + public ForeignKeyConstraint withName(List name) { + return (ForeignKeyConstraint) super.withName(name); + } + + @Override + public ForeignKeyConstraint withName(String name) { + return (ForeignKeyConstraint) super.withName(name); + } + + public ForeignKeyConstraint withIndexName(String value) { + setIndexName(value); + return this; + } + + public ForeignKeyConstraint withColumnsNames(List value) { + setColumnsNames(value); + return this; + } + + public ForeignKeyConstraint withColumns(List value) { + setColumns(value); + return this; + } + + public ForeignKeyConstraint addColumns(KeyElement... values) { + return addColumns(java.util.Arrays.asList(values)); + } + + public ForeignKeyConstraint addColumns(Collection values) { + if (columns == null) { + columns = new java.util.ArrayList<>(); + } + columns.addAll(values); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java deleted file mode 100644 index c877a1ad9e..0000000000 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java +++ /dev/null @@ -1,211 +0,0 @@ -/*- - * #%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.statement.create.table; - -import net.sf.jsqlparser.schema.Table; -import net.sf.jsqlparser.statement.ReferentialAction; -import net.sf.jsqlparser.statement.ReferentialAction.Action; -import net.sf.jsqlparser.statement.ReferentialAction.Type; -import net.sf.jsqlparser.statement.select.PlainSelect; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -public class ForeignKeyIndex extends NamedConstraint { - - private Table table; - private List referencedColumnNames; - private Set referentialActions = new LinkedHashSet<>(2); - - public Table getTable() { - return table; - } - - public void setTable(Table table) { - this.table = table; - } - - public List getReferencedColumnNames() { - return referencedColumnNames; - } - - public void setReferencedColumnNames(List referencedColumnNames) { - this.referencedColumnNames = referencedColumnNames; - } - - /** - * @param type - * @param action - */ - public void setReferentialAction(Type type, Action action) { - setReferentialAction(type, action, true); - } - - public ForeignKeyIndex withReferentialAction(Type type, Action action) { - setReferentialAction(type, action); - return this; - } - - /** - * @param type - */ - public void removeReferentialAction(Type type) { - setReferentialAction(type, null, false); - } - - /** - * @param type - * @return - */ - public ReferentialAction getReferentialAction(Type type) { - return referentialActions.stream().filter(ra -> type.equals(ra.getType())).findFirst() - .orElse(null); - } - - private void setReferentialAction(Type type, Action action, boolean set) { - ReferentialAction found = getReferentialAction(type); - if (set) { - if (found == null) { - referentialActions.add(new ReferentialAction(type, action)); - } else { - found.setAction(action); - } - } else if (found != null) { - referentialActions.remove(found); - } - } - - @Deprecated - public String getOnDeleteReferenceOption() { - ReferentialAction a = getReferentialAction(Type.DELETE); - return a == null ? null : a.getAction().getAction(); - } - - @Deprecated - public void setOnDeleteReferenceOption(String onDeleteReferenceOption) { - if (onDeleteReferenceOption == null) { - removeReferentialAction(Type.DELETE); - } else { - setReferentialAction(Type.DELETE, Action.from(onDeleteReferenceOption)); - } - } - - @Deprecated - public String getOnUpdateReferenceOption() { - ReferentialAction a = getReferentialAction(Type.UPDATE); - return a == null ? null : a.getAction().getAction(); - } - - @Deprecated - public void setOnUpdateReferenceOption(String onUpdateReferenceOption) { - if (onUpdateReferenceOption == null) { - removeReferentialAction(Type.UPDATE); - } else { - setReferentialAction(Type.UPDATE, Action.from(onUpdateReferenceOption)); - } - } - - @Override - public String toString() { - StringBuilder b = new StringBuilder(super.toString()).append(" REFERENCES ").append(table) - .append(PlainSelect.getStringList(getReferencedColumnNames(), true, true)); - referentialActions.forEach(b::append); - return b.toString(); - } - - public ForeignKeyIndex withTable(Table table) { - this.setTable(table); - return this; - } - - public ForeignKeyIndex withReferencedColumnNames(List referencedColumnNames) { - this.setReferencedColumnNames(referencedColumnNames); - return this; - } - - public ForeignKeyIndex withOnDeleteReferenceOption(String onDeleteReferenceOption) { - this.setOnDeleteReferenceOption(onDeleteReferenceOption); - return this; - } - - public ForeignKeyIndex withOnUpdateReferenceOption(String onUpdateReferenceOption) { - this.setOnUpdateReferenceOption(onUpdateReferenceOption); - return this; - } - - public ForeignKeyIndex addReferencedColumnNames(String... referencedColumnNames) { - List collection = - Optional.ofNullable(getReferencedColumnNames()).orElseGet(ArrayList::new); - Collections.addAll(collection, referencedColumnNames); - return this.withReferencedColumnNames(collection); - } - - public ForeignKeyIndex addReferencedColumnNames(Collection referencedColumnNames) { - List collection = - Optional.ofNullable(getReferencedColumnNames()).orElseGet(ArrayList::new); - collection.addAll(referencedColumnNames); - return this.withReferencedColumnNames(collection); - } - - @Override - public ForeignKeyIndex withType(String type) { - return (ForeignKeyIndex) super.withType(type); - } - - @Override - public ForeignKeyIndex withUsing(String using) { - return (ForeignKeyIndex) super.withUsing(using); - } - - @Override - public ForeignKeyIndex withName(List name) { - return (ForeignKeyIndex) super.withName(name); - } - - @Override - public ForeignKeyIndex withName(String name) { - return (ForeignKeyIndex) super.withName(name); - } - - @Override - public ForeignKeyIndex withColumnsNames(List list) { - return (ForeignKeyIndex) super.withColumnsNames(list); - } - - @Override - public ForeignKeyIndex withColumns(List columns) { - return (ForeignKeyIndex) super.withColumns(columns); - } - - @Override - public ForeignKeyIndex addColumns(ColumnParams... functionDeclarationParts) { - return (ForeignKeyIndex) super.addColumns(functionDeclarationParts); - } - - @Override - public ForeignKeyIndex addColumns(Collection functionDeclarationParts) { - return (ForeignKeyIndex) super.addColumns(functionDeclarationParts); - } - - @Override - public ForeignKeyIndex withIndexSpec(List idxSpec) { - return (ForeignKeyIndex) super.withIndexSpec(idxSpec); - } - - @Override - public ForeignKeyIndex withIndexKeyword(String indexKeyword) { - return (ForeignKeyIndex) super.withIndexKeyword(indexKeyword); - } -} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java new file mode 100644 index 0000000000..24208db933 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java @@ -0,0 +1,188 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.LinkedHashSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.ReferentialAction; +import net.sf.jsqlparser.statement.ReferentialAction.Action; +import net.sf.jsqlparser.statement.ReferentialAction.Type; + +/** The target and actions of a column- or table-level foreign-key reference. */ +public class ForeignKeyReference implements Serializable { + + public enum MatchType { + FULL, PARTIAL, SIMPLE + } + + private Table table; + private String constraintName; + + /** Optional name of a column REFERENCES constraint. Table constraints own their name. */ + public String getConstraintName() { + return constraintName; + } + + public void setConstraintName(String constraintName) { + this.constraintName = constraintName; + } + + private List referencedColumnNames; + private MatchType matchType; + private boolean usingPeriod; + private ConstraintAttributes constraintAttributes; + private final Set referentialActions = new LinkedHashSet<>(2); + + /** + * Attributes of a column REFERENCES clause; table constraints own their attributes on + * NamedConstraint. + */ + public ConstraintAttributes getConstraintAttributes() { + return constraintAttributes; + } + + public void setConstraintAttributes(ConstraintAttributes constraintAttributes) { + this.constraintAttributes = constraintAttributes; + } + + /** Whether the final explicitly referenced column is prefixed with PERIOD. */ + public boolean isUsingPeriod() { + return usingPeriod; + } + + public void setUsingPeriod(boolean usingPeriod) { + this.usingPeriod = usingPeriod; + } + + public ForeignKeyReference withUsingPeriod(boolean usingPeriod) { + setUsingPeriod(usingPeriod); + return this; + } + + public Table getTable() { + return table; + } + + public void setTable(Table table) { + this.table = table; + } + + public List getReferencedColumnNames() { + return referencedColumnNames; + } + + public void setReferencedColumnNames(List referencedColumnNames) { + this.referencedColumnNames = referencedColumnNames; + } + + public MatchType getMatchType() { + return matchType; + } + + public void setMatchType(MatchType matchType) { + this.matchType = matchType; + } + + public Set getReferentialActions() { + return referentialActions; + } + + public ReferentialAction getReferentialAction(Type type) { + return referentialActions.stream().filter(action -> type.equals(action.getType())) + .findFirst() + .orElse(null); + } + + public void setReferentialAction(Type type, Action action) { + ReferentialAction current = getReferentialAction(type); + if (current == null) { + referentialActions.add(new ReferentialAction(type, action)); + } else { + current.setAction(action); + } + } + + public void removeReferentialAction(Type type) { + // Actions are mutable; their hash code can change after insertion into the set. + referentialActions.removeIf(action -> type.equals(action.getType())); + } + + public ForeignKeyReference withTable(Table table) { + setTable(table); + return this; + } + + public ForeignKeyReference withReferencedColumnNames(List referencedColumnNames) { + setReferencedColumnNames(referencedColumnNames); + return this; + } + + public ForeignKeyReference withMatchType(MatchType matchType) { + setMatchType(matchType); + return this; + } + + public ForeignKeyReference withReferentialAction(Type type, Action action) { + setReferentialAction(type, action); + return this; + } + + public ForeignKeyReference addReferencedColumnNames(String... referencedColumnNames) { + List collection = Optional.ofNullable(getReferencedColumnNames()) + .orElseGet(ArrayList::new); + Collections.addAll(collection, referencedColumnNames); + return withReferencedColumnNames(collection); + } + + public ForeignKeyReference addReferencedColumnNames( + Collection referencedColumnNames) { + List collection = Optional.ofNullable(getReferencedColumnNames()) + .orElseGet(ArrayList::new); + collection.addAll(referencedColumnNames); + return withReferencedColumnNames(collection); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (constraintName != null) { + builder.append("CONSTRAINT ").append(constraintName).append(' '); + } + builder.append("REFERENCES ").append(table); + if (referencedColumnNames != null) { + builder.append('('); + for (int i = 0; i < referencedColumnNames.size(); i++) { + if (i > 0) { + builder.append(", "); + } + if (usingPeriod && i == referencedColumnNames.size() - 1) { + builder.append("PERIOD "); + } + builder.append(referencedColumnNames.get(i)); + } + builder.append(')'); + } + if (matchType != null) { + builder.append(" MATCH ").append(matchType); + } + referentialActions.forEach(builder::append); + if (constraintAttributes != null) { + constraintAttributes.appendTo(builder); + } + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignTableOptions.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignTableOptions.java new file mode 100644 index 0000000000..1ff2017d6d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignTableOptions.java @@ -0,0 +1,52 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** PostgreSQL foreign server and optional table-level FDW options. */ +public class ForeignTableOptions implements Serializable { + private String server; + private List options; + + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append("SERVER ").append(server); + if (options != null && !options.isEmpty()) { + builder.append(' '); + ForeignDataOption.appendOptionsTo(builder, options, expressionPrinter); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/GeneratedColumnDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/table/GeneratedColumnDefinition.java new file mode 100644 index 0000000000..ac069d6e45 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/GeneratedColumnDefinition.java @@ -0,0 +1,101 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** A parenthesized generated column expression, distinct from an identity declaration. */ +public class GeneratedColumnDefinition implements Serializable { + public enum Storage { + STORED, VIRTUAL + } + + private Expression expression; + private boolean generatedAlways; + private Storage storage; + + public GeneratedColumnDefinition(Expression expression) { + setExpression(expression); + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = Objects.requireNonNull(expression, "expression"); + } + + public boolean isGeneratedAlways() { + return generatedAlways; + } + + public void setGeneratedAlways(boolean generatedAlways) { + this.generatedAlways = generatedAlways; + } + + /** Returns null when the SQL does not specify STORED or VIRTUAL. */ + public Storage getStorage() { + return storage; + } + + public void setStorage(Storage storage) { + this.storage = storage; + } + + /** + * Returns an unmodifiable token snapshot. Use the setters or edit {@link #getExpression()} to + * change the definition; later changes do not affect previously returned snapshots. + */ + public List getTokens() { + List tokens = new ArrayList<>(); + if (generatedAlways) { + tokens.add("GENERATED"); + tokens.add("ALWAYS"); + } + tokens.add("AS"); + tokens.add("(" + expression + ")"); + if (storage != null) { + tokens.add(storage.name()); + } + return Collections.unmodifiableList(tokens); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + if (generatedAlways) { + builder.append("GENERATED ALWAYS "); + } + appendExpressionTo(builder, expression, expressionPrinter); + if (storage != null) { + builder.append(' ').append(storage); + } + return builder; + } + + /** Prints the common AS (expression) body of declarations and expression replacements. */ + public static StringBuilder appendExpressionTo(StringBuilder builder, Expression expression, + Consumer expressionPrinter) { + builder.append("AS ("); + expressionPrinter.accept(expression); + return builder.append(')'); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/IdentityDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/table/IdentityDefinition.java new file mode 100644 index 0000000000..02ed06a4f0 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/IdentityDefinition.java @@ -0,0 +1,59 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.schema.Sequence; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** The generation mode and sequence parameters of a GENERATED ... AS IDENTITY clause. */ +public class IdentityDefinition implements Serializable { + public enum GenerationMode { + ALWAYS, BY_DEFAULT + } + + private GenerationMode generationMode; + private List parameters; + + public IdentityDefinition(GenerationMode generationMode) { + this.generationMode = generationMode; + } + + public GenerationMode getGenerationMode() { + return generationMode; + } + + public void setGenerationMode(GenerationMode generationMode) { + this.generationMode = generationMode; + } + + /** Returns null when no parenthesized sequence options were specified. */ + public List getParameters() { + return parameters; + } + + public void setParameters(List parameters) { + this.parameters = parameters == null ? null : new ArrayList<>(parameters); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder("GENERATED "); + sql.append(generationMode == GenerationMode.ALWAYS ? "ALWAYS" : "BY DEFAULT") + .append(" AS IDENTITY"); + if (parameters != null) { + sql.append(" (").append(PlainSelect.getStringList(parameters, false, false)) + .append(')'); + } + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java index 114b30d147..adde4cd056 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java @@ -9,90 +9,58 @@ */ package net.sf.jsqlparser.statement.create.table; -import static java.util.stream.Collectors.toList; - import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Optional; +import java.util.Objects; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.statement.select.PlainSelect; -public class Index implements Serializable { +/** An index declaration, independent of table and column constraints. */ +public class Index implements TableElement, Serializable, KeyColumnSource, IndexOptionSource { + public enum Kind { + UNIQUE, INDEX, FULLTEXT, SPATIAL, OTHER + } private final List name = new ArrayList<>(); private String type; - private String using; - private List columns; - private List idxSpec; - private String commentText; + private Kind kind = Kind.OTHER; + private List columns; private String indexKeyword; + private IndexOptions indexOptions = new IndexOptions(); + private List tailParameters = new ArrayList<>(); - public List getColumnsNames() { - return columns.stream() - .map(ColumnParams::getColumnName) - .collect(toList()); - } - - public void setColumnsNames(List list) { - if (list == null) { - this.columns = Collections.emptyList(); - } else { - this.columns = list.stream().map(ColumnParams::new).collect(toList()); - } - } - - @Deprecated - public List getColumnWithParams() { - return getColumns(); + /** Opaque accepted suffixes rendered after the structured index options. */ + public List getTailParameters() { + return tailParameters; } - @Deprecated - public void setColumnNamesWithParams(List list) { - setColumns(list); + public void setTailParameters(List values) { + tailParameters = values; } - public List getColumns() { - return columns; - } - - public void setColumns(List columns) { - this.columns = columns; - } - - public Index withColumns(List columns) { - setColumns(columns); + public Index withTailParameters(List values) { + setTailParameters(values); return this; } - public Index addColumns(ColumnParams... functionDeclarationParts) { - List collection = Optional.ofNullable(getColumns()).orElseGet(ArrayList::new); - Collections.addAll(collection, functionDeclarationParts); - return this.withColumns(collection); - } - - public Index addColumns(Collection functionDeclarationParts) { - List collection = Optional.ofNullable(getColumns()).orElseGet(ArrayList::new); - collection.addAll(functionDeclarationParts); - return this.withColumns(collection); - } - public String getName() { return name.isEmpty() ? null : String.join(".", name); } - public void setName(String name) { - this.name.clear(); - if (name != null) { - this.name.add(name); + public void setName(String value) { + name.clear(); + if (value != null) { + name.add(value); } } - public void setName(List name) { - this.name.clear(); - this.name.addAll(name); + public void setName(List value) { + name.clear(); + name.addAll(value); } public List getNameParts() { @@ -103,151 +71,198 @@ public String getType() { return type; } - public void setType(String string) { - type = string; + /** Retains SQL spelling and refreshes the index-only classification. */ + public void setType(String value) { + type = value; + if (value == null) { + kind = Kind.OTHER; + return; + } + String normalized = value.trim().toUpperCase(java.util.Locale.ROOT); + switch (normalized.split("\\s+", 2)[0]) { + case "UNIQUE": + kind = Kind.UNIQUE; + break; + case "FULLTEXT": + kind = Kind.FULLTEXT; + break; + case "SPATIAL": + kind = Kind.SPATIAL; + break; + case "PRIMARY": + case "FOREIGN": + case "CHECK": + case "EXCLUDE": + case "DEFAULT": + case "NOT": + kind = Kind.OTHER; + break; + default: + kind = normalized.equals("INDEX") || normalized.equals("KEY") + || normalized.endsWith(" INDEX") || normalized.endsWith(" KEY") + ? Kind.INDEX + : Kind.OTHER; + } + } + + public Kind getKind() { + return kind; } - public Index withColumnsNames(List list) { - setColumnsNames(list); + /** Changes classification metadata without rewriting SQL. */ + public void setKind(Kind value) { + kind = value; + } + + public Index withKind(Kind value) { + setKind(value); return this; } - public String getUsing() { - return using; + public String getIndexKeyword() { + return indexKeyword; + } + + public void setIndexKeyword(String value) { + indexKeyword = value; } - /** - * In postgresql, the index type (Btree, GIST, etc.) is indicated with a USING clause. Please - * note that: Oracle - the type might be BITMAP, indicating a bitmap kind of index MySQL - the - * type might be FULLTEXT or SPATIAL - * - * @param using - */ - public void setUsing(String using) { - this.using = using; + public Index withIndexKeyword(String value) { + setIndexKeyword(value); + return this; } - public List getIndexSpec() { - return idxSpec; + @Override + public List getColumns() { + return columns; } - public void setIndexSpec(List idxSpec) { - this.idxSpec = idxSpec; + @Override + public void setColumns(List value) { + columns = value; } - public Index withIndexSpec(List idxSpec) { - setIndexSpec(idxSpec); + public Index withColumns(List value) { + setColumns(value); return this; } - public void setIndexKeyword(String indexKeyword) { - this.indexKeyword = indexKeyword; + public Index withColumnsNames(List value) { + setColumnsNames(value); + return this; } - public String getIndexKeyword() { - return indexKeyword; + public Index addColumns(KeyElement... values) { + return addColumns(java.util.Arrays.asList(values)); } - public Index withIndexKeyword(String indexKeyword) { - this.setIndexKeyword(indexKeyword); + public Index addColumns(Collection values) { + if (columns == null) { + columns = new ArrayList<>(); + } + columns.addAll(values); return this; } @Override - public String toString() { - String idxSpecText = PlainSelect.getStringList(idxSpec, false, false); - String keyword = (indexKeyword != null) ? " " + indexKeyword : ""; - String head = - (type != null ? type : "") + - keyword + - (!name.isEmpty() ? " " + getName() : "") + - (using != null ? " USING " + using : ""); - - String tail = (columns != null && !columns.isEmpty() - ? PlainSelect.getStringList(columns, true, true) - : "") - + (!idxSpecText.isEmpty() ? " " + idxSpecText : ""); + public IndexOptions getIndexOptions() { + return indexOptions; + } - return tail.isEmpty() ? head : head + " " + tail; + public void setIndexOptions(IndexOptions options) { + indexOptions = Objects.requireNonNull(options, "indexOptions"); } - public Index withType(String type) { - this.setType(type); + public Index withUsing(String value) { + setUsing(value); return this; } - public Index withUsing(String using) { - this.setUsing(using); + public Index withIndexSpec(List value) { + setIndexSpec(value); return this; } - public Index withName(List name) { - this.setName(name); + public Index withSpecOptions(List value) { + setSpecOptions(value); return this; } - public Index withName(String name) { - this.setName(name); + public Index withClustering(IndexOptions.Clustering value) { + setClustering(value); return this; } - public String getCommentText() { - return commentText; + public Index withType(String value) { + setType(value); + return this; } - public void setCommentText(String commentText) { - this.commentText = commentText; + public Index withName(String value) { + setName(value); + return this; } - public static class ColumnParams implements Serializable { - public final String columnName; - public final List params; - private final Expression expression; + public Index withName(List value) { + setName(value); + return this; + } - public ColumnParams(String columnName) { - this.columnName = columnName; - this.params = null; - this.expression = null; + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } + + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { + int start = sql.length(); + appendDeclarationTo(sql, start); + appendColumnsAndSpecTo(sql, start, expressionPrinter); + appendIndexOptionsTo(sql, expressionPrinter); + if (tailParameters != null && !tailParameters.isEmpty()) { + sql.append(' ').append(PlainSelect.getStringList(tailParameters, false, false)); } + } - public ColumnParams(String columnName, List params) { - this.columnName = columnName; - this.params = params; - this.expression = null; + private void appendDeclarationTo(StringBuilder sql, int start) { + if (type != null) { + sql.append(type); } - - public ColumnParams(Expression expression) { - this.columnName = null; - this.params = null; - this.expression = expression; + if (indexKeyword != null && (type == null || !type.toUpperCase(java.util.Locale.ROOT) + .endsWith(indexKeyword.toUpperCase(java.util.Locale.ROOT)))) { + appendWord(sql, start, indexKeyword); } - - public ColumnParams(Expression expression, List params) { - this.columnName = null; - this.params = params; - this.expression = expression; + if (!name.isEmpty()) { + appendWord(sql, start, getName()); } - - public String getColumnName() { - return expression != null ? expression.toString() : columnName; + if (getUsing() != null) { + appendWord(sql, start, "USING " + getUsing()); } + sql.append(nullsDistinctClause()).append(clusteringClause()); + } - public List getParams() { - return params; - } + /** Separates {@code word} from earlier index words, without a leading space. */ + private static void appendWord(StringBuilder sql, int start, String word) { + appendSeparator(sql, start); + sql.append(word); + } - public Expression getExpression() { - return expression; + private static void appendSeparator(StringBuilder sql, int start) { + if (sql.length() > start) { + sql.append(' '); } + } - public boolean isExpression() { - return expression != null; + private void appendColumnsAndSpecTo(StringBuilder sql, int start, + Consumer expressionPrinter) { + if (columns != null && !columns.isEmpty()) { + appendSeparator(sql, start); + appendColumnsTo(sql, expressionPrinter); } - - @Override - public String toString() { - String head = expression != null ? "(" + expression + ")" : columnName; - return head + (params != null ? " " + String.join(" ", params) : ""); + String spec = PlainSelect.getStringList(getIndexSpec(), false, false); + if (!spec.isEmpty()) { + appendWord(sql, start, spec); } } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOption.java new file mode 100644 index 0000000000..5218b37c06 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOption.java @@ -0,0 +1,100 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** A named option with an optional expression value and explicit equals spelling. */ +public class IndexOption implements Serializable { + private String name; + private Expression value; + private boolean useEquals; + + public IndexOption() {} + + public IndexOption(String name, Expression value, boolean useEquals) { + this.name = name; + this.value = value; + this.useEquals = useEquals; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Expression getValue() { + return value; + } + + public void setValue(Expression value) { + this.value = value; + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + public IndexOption withName(String name) { + setName(name); + return this; + } + + public IndexOption withValue(Expression value) { + setValue(value); + return this; + } + + public IndexOption withUseEquals(boolean useEquals) { + setUseEquals(useEquals); + return this; + } + + @Override + public String toString() { + if (value == null) { + return name; + } + StringBuilder builder = new StringBuilder(); + return appendTo(builder, expression -> builder.append(expression)).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { + builder.append(name); + if (value != null) { + builder.append(useEquals ? " = " : " "); + expressionPrinter.accept(value); + } + return builder; + } + + public static StringBuilder appendListTo(StringBuilder builder, List options, + Consumer expressionPrinter) { + builder.append('('); + for (int i = 0; i < options.size(); i++) { + if (i > 0) { + builder.append(", "); + } + options.get(i).appendTo(builder, expressionPrinter); + } + return builder.append(')'); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptionSource.java b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptionSource.java new file mode 100644 index 0000000000..4c016d32a5 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptionSource.java @@ -0,0 +1,103 @@ +/*- + * #%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.statement.create.table; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** Access to the single options object of an index or an index-backed constraint. */ +public interface IndexOptionSource { + IndexOptions getIndexOptions(); + + default String getUsing() { + return getIndexOptions().getUsing(); + } + + default void setUsing(String value) { + getIndexOptions().setUsing(value); + } + + default List getIndexSpec() { + return getIndexOptions().getIndexSpec(); + } + + default void setIndexSpec(List value) { + getIndexOptions().setIndexSpec(value); + } + + default List getSpecOptions() { + return getIndexOptions().getSpecOptions(); + } + + default void setSpecOptions(List value) { + getIndexOptions().setSpecOptions(value); + } + + default String getCommentText() { + return getIndexOptions().getCommentText(); + } + + default void setCommentText(String value) { + getIndexOptions().setCommentText(value); + } + + default IndexOptions.Clustering getClustering() { + return getIndexOptions().getClustering(); + } + + default void setClustering(IndexOptions.Clustering value) { + getIndexOptions().setClustering(value); + } + + default Boolean getNullsDistinct() { + return getIndexOptions().getNullsDistinct(); + } + + default void setNullsDistinct(Boolean value) { + getIndexOptions().setNullsDistinct(value); + } + + default List getIncludeColumns() { + return getIndexOptions().getIncludeColumns(); + } + + default void setIncludeColumns(List value) { + getIndexOptions().setIncludeColumns(value); + } + + default List getStorageParameters() { + return getIndexOptions().getStorageParameters(); + } + + default void setStorageParameters(List value) { + getIndexOptions().setStorageParameters(value); + } + + default String getTableSpace() { + return getIndexOptions().getTableSpace(); + } + + default void setTableSpace(String value) { + getIndexOptions().setTableSpace(value); + } + + default String nullsDistinctClause() { + return getIndexOptions().nullsDistinctClause(); + } + + default String clusteringClause() { + return getIndexOptions().clusteringClause(); + } + + default void appendIndexOptionsTo(StringBuilder sql, Consumer expressionPrinter) { + getIndexOptions().appendTo(sql, expressionPrinter); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptions.java b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptions.java new file mode 100644 index 0000000000..f6050b4bca --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptions.java @@ -0,0 +1,171 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** Index implementation options explicitly present in an index or supporting-key declaration. */ +public class IndexOptions implements Serializable { + public enum Clustering { + CLUSTERED, NONCLUSTERED + } + + private String using; + private List specOptions; + private String commentText; + private Clustering clustering; + private Boolean nullsDistinct; + private List includeColumns; + private List storageParameters; + private String tableSpace; + + public String getUsing() { + return using; + } + + public void setUsing(String using) { + this.using = using; + } + + /** + * Returns the mutable, ordered options written after the key list, or null when unset. An index + * type written before the key list is {@link #getUsing()} instead. + */ + public List getSpecOptions() { + return specOptions; + } + + /** Copies the list membership and retains the option nodes; null clears the options. */ + public void setSpecOptions(List specOptions) { + this.specOptions = specOptions == null ? null : new ArrayList<>(specOptions); + } + + public IndexOptions addSpecOptions(IndexSpecOption... options) { + return addSpecOptions(Arrays.asList(options)); + } + + public IndexOptions addSpecOptions(Collection options) { + if (specOptions == null) { + specOptions = new ArrayList<>(); + } + specOptions.addAll(options); + return this; + } + + /** + * Returns an unmodifiable token snapshot of {@link #getSpecOptions()}, such as + * {@code [KEY_BLOCK_SIZE, =, 8, COMMENT, 'text']}, or null when unset. Edit the option nodes + * instead of the snapshot. + */ + public List getIndexSpec() { + if (specOptions == null) { + return null; + } + List tokens = new ArrayList<>(); + for (IndexSpecOption option : specOptions) { + tokens.addAll(option.getTokens()); + } + return Collections.unmodifiableList(tokens); + } + + /** + * Replaces all options after the key list with one opaque option holding a copy of the tokens, + * without parsing them. Null clears the options; an empty list stores no options. + */ + public void setIndexSpec(List indexSpec) { + if (indexSpec == null) { + specOptions = null; + return; + } + specOptions = new ArrayList<>(); + if (!indexSpec.isEmpty()) { + specOptions.add(IndexSpecOption.raw(indexSpec)); + } + } + + public String getCommentText() { + return commentText; + } + + public void setCommentText(String commentText) { + this.commentText = commentText; + } + + public Clustering getClustering() { + return clustering; + } + + public void setClustering(Clustering clustering) { + this.clustering = clustering; + } + + public Boolean getNullsDistinct() { + return nullsDistinct; + } + + public void setNullsDistinct(Boolean nullsDistinct) { + this.nullsDistinct = nullsDistinct; + } + + public List getIncludeColumns() { + return includeColumns; + } + + public void setIncludeColumns(List includeColumns) { + this.includeColumns = includeColumns == null ? null : new ArrayList<>(includeColumns); + } + + public List getStorageParameters() { + return storageParameters; + } + + public void setStorageParameters(List storageParameters) { + this.storageParameters = + storageParameters == null ? null : new ArrayList<>(storageParameters); + } + + public String getTableSpace() { + return tableSpace; + } + + public void setTableSpace(String tableSpace) { + this.tableSpace = tableSpace; + } + + public String nullsDistinctClause() { + return nullsDistinct == null ? "" + : nullsDistinct ? " NULLS DISTINCT" : " NULLS NOT DISTINCT"; + } + + public String clusteringClause() { + return clustering == null ? "" : " " + clustering; + } + + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { + if (includeColumns != null) { + sql.append(" INCLUDE ").append(PlainSelect.getStringList(includeColumns, true, true)); + } + if (storageParameters != null) { + sql.append(" WITH "); + IndexOption.appendListTo(sql, storageParameters, expressionPrinter); + } + if (tableSpace != null) { + sql.append(" USING INDEX TABLESPACE ").append(tableSpace); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/IndexSpecOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexSpecOption.java new file mode 100644 index 0000000000..9ff6fa655b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexSpecOption.java @@ -0,0 +1,200 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import net.sf.jsqlparser.expression.StringValue; + +/** + * An option written after the key list of an index or key definition. MySQL's index options + * (KEY_BLOCK_SIZE, USING index type, WITH PARSER, COMMENT, VISIBLE/INVISIBLE, ENGINE_ATTRIBUTE and + * SECONDARY_ENGINE_ATTRIBUTE) are typed; other accepted tokens are retained as opaque + * {@link Kind#OTHER} options. The kind is fixed; replace the node to change it. + */ +public class IndexSpecOption implements Serializable { + + public enum Kind { + KEY_BLOCK_SIZE, INDEX_TYPE, PARSER, COMMENT, VISIBILITY, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, OTHER + } + + private final Kind kind; + private String value; + private StringValue comment; + private boolean useEquals; + private boolean visible; + private List tokens; + + private IndexSpecOption(Kind kind) { + this.kind = kind; + } + + /** + * Creates a valued option. {@code value} keeps its SQL spelling: a number for KEY_BLOCK_SIZE, + * an index type or parser name, or a quoted string literal for COMMENT and the engine + * attributes. + */ + public static IndexSpecOption of(Kind kind, String value) { + return of(kind, value, false); + } + + /** Like {@link #of(Kind, String)}; {@code useEquals} requires a kind that accepts '='. */ + public static IndexSpecOption of(Kind kind, String value, boolean useEquals) { + IndexSpecOption option = new IndexSpecOption(Objects.requireNonNull(kind, "kind")); + option.setValue(value); + option.setUseEquals(useEquals); + return option; + } + + /** + * Creates a COMMENT option. The literal keeps its quotes and escaping, so + * {@link StringValue#getNotExcapedValue()} returns the stored comment text. + */ + public static IndexSpecOption comment(StringValue comment) { + IndexSpecOption option = new IndexSpecOption(Kind.COMMENT); + option.setComment(comment); + return option; + } + + public static IndexSpecOption visibility(boolean visible) { + IndexSpecOption option = new IndexSpecOption(Kind.VISIBILITY); + option.visible = visible; + return option; + } + + /** Creates an opaque option from a copy of {@code tokens}; the tokens are never parsed. */ + public static IndexSpecOption raw(List tokens) { + IndexSpecOption option = new IndexSpecOption(Kind.OTHER); + option.setTokens(tokens); + return option; + } + + public static IndexSpecOption raw(String... tokens) { + return raw(Arrays.asList(tokens)); + } + + public Kind getKind() { + return kind; + } + + /** + * The SQL spelling of the option value, or null for VISIBILITY and opaque options. For COMMENT, + * this is the literal's SQL text, such as {@code 'it\'s'}; use {@link #getComment()} for its + * decoded text. + */ + public String getValue() { + return kind == Kind.COMMENT ? comment.toString() : value; + } + + /** + * Replaces the value with its SQL spelling. A COMMENT value is read as a string literal and + * keeps the escaping of the literal it replaces. + */ + public void setValue(String value) { + if (kind == Kind.VISIBILITY || kind == Kind.OTHER) { + throw new IllegalStateException(kind + " options have no value"); + } + Objects.requireNonNull(value, "value"); + if (kind == Kind.COMMENT) { + setComment(new StringValue(value) + .withEscaping(comment == null ? null : comment.getEscaping())); + } else { + this.value = value; + } + } + + /** The literal of a COMMENT option, otherwise null. */ + public StringValue getComment() { + return comment; + } + + public void setComment(StringValue comment) { + if (kind != Kind.COMMENT) { + throw new IllegalStateException(kind + " options have no comment"); + } + this.comment = Objects.requireNonNull(comment, "comment"); + } + + public boolean isUseEquals() { + return useEquals; + } + + /** + * Selects the optional '=' of KEY_BLOCK_SIZE, ENGINE_ATTRIBUTE and SECONDARY_ENGINE_ATTRIBUTE. + */ + public void setUseEquals(boolean useEquals) { + if (useEquals && kind != Kind.KEY_BLOCK_SIZE && kind != Kind.ENGINE_ATTRIBUTE + && kind != Kind.SECONDARY_ENGINE_ATTRIBUTE) { + throw new IllegalStateException(kind + " options do not use '='"); + } + this.useEquals = useEquals; + } + + /** VISIBLE (true) or INVISIBLE (false) for a VISIBILITY option, otherwise null. */ + public Boolean getVisible() { + return kind == Kind.VISIBILITY ? visible : null; + } + + public void setVisible(boolean visible) { + if (kind != Kind.VISIBILITY) { + throw new IllegalStateException(kind + " options have no visibility"); + } + this.visible = visible; + } + + /** + * Returns an unmodifiable, detached token snapshot: the keywords, an optional '=' and the value + * of a typed option, or a copy of an opaque option's tokens. + */ + public List getTokens() { + if (kind == Kind.OTHER) { + return Collections.unmodifiableList(new ArrayList<>(tokens)); + } + List result = new ArrayList<>(Arrays.asList(keyword().split(" "))); + if (useEquals) { + result.add("="); + } + if (getValue() != null) { + result.add(getValue()); + } + return Collections.unmodifiableList(result); + } + + /** Replaces the tokens of an opaque option with a copy; typed options reject raw tokens. */ + public void setTokens(List tokens) { + if (kind != Kind.OTHER) { + throw new IllegalStateException( + "Typed " + kind + " options cannot be replaced with raw tokens"); + } + this.tokens = new ArrayList<>(Objects.requireNonNull(tokens, "tokens")); + } + + private String keyword() { + switch (kind) { + case INDEX_TYPE: + return "USING"; + case PARSER: + return "WITH PARSER"; + case VISIBILITY: + return visible ? "VISIBLE" : "INVISIBLE"; + default: + return kind.name(); + } + } + + @Override + public String toString() { + return String.join(" ", getTokens()); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/KeyColumnSource.java b/src/main/java/net/sf/jsqlparser/statement/create/table/KeyColumnSource.java new file mode 100644 index 0000000000..9fec2adea0 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/KeyColumnSource.java @@ -0,0 +1,51 @@ +/*- + * #%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.statement.create.table; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.expression.Expression; + +/** Shared key access without making a constraint an index. */ +public interface KeyColumnSource { + List getColumns(); + + void setColumns(List columns); + + /** Mutable rendered snapshot; use getColumns() for structured edits. */ + default List getColumnsNames() { + return getColumns() == null ? new ArrayList<>() + : getColumns().stream() + .map(KeyElement::toString).collect(Collectors.toList()); + } + + /** + * Replaces keys with opaque names, without parsing or retaining expression options. A null or + * empty list clears the keys and leaves the structured list editable. + */ + default void setColumnsNames(List names) { + setColumns(names == null ? new ArrayList<>() + : names.stream() + .map(KeyElement::new).collect(Collectors.toList())); + } + + default void appendColumnsTo(StringBuilder sql, Consumer expressionPrinter) { + sql.append('('); + for (int i = 0; i < getColumns().size(); i++) { + if (i > 0) { + sql.append(", "); + } + getColumns().get(i).appendTo(sql, expressionPrinter); + } + sql.append(')'); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/KeyConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/KeyConstraint.java new file mode 100644 index 0000000000..979fe44761 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/KeyConstraint.java @@ -0,0 +1,169 @@ +/*- + * #%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.statement.create.table; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** PRIMARY KEY or UNIQUE declaration with explicitly supplied supporting-index options. */ +public class KeyConstraint extends NamedConstraint implements KeyColumnSource, IndexOptionSource { + private List columns; + private String indexName; + private String indexKeyword; + private IndexOptions indexOptions = new IndexOptions(); + + public String getIndexName() { + return indexName; + } + + public void setIndexName(String value) { + indexName = value; + } + + public KeyConstraint withIndexName(String value) { + setIndexName(value); + return this; + } + + public String getIndexKeyword() { + return indexKeyword; + } + + public void setIndexKeyword(String value) { + indexKeyword = value; + } + + public KeyConstraint withIndexKeyword(String value) { + setIndexKeyword(value); + return this; + } + + @Override + public KeyConstraint withKind(ConstraintKind value) { + setKind(value); + return this; + } + + @Override + public KeyConstraint withUseConstraintKeyword(boolean value) { + setUseConstraintKeyword(value); + return this; + } + + @Override + public List getColumns() { + return columns; + } + + @Override + public void setColumns(List value) { + columns = value; + } + + public KeyConstraint withColumns(List value) { + setColumns(value); + return this; + } + + public KeyConstraint withColumnsNames(List value) { + setColumnsNames(value); + return this; + } + + public KeyConstraint addColumns(KeyElement... values) { + return addColumns(java.util.Arrays.asList(values)); + } + + public KeyConstraint addColumns(Collection values) { + if (columns == null) { + columns = new ArrayList<>(); + } + columns.addAll(values); + return this; + } + + @Override + public IndexOptions getIndexOptions() { + return indexOptions; + } + + public void setIndexOptions(IndexOptions options) { + indexOptions = Objects.requireNonNull(options, "indexOptions"); + } + + public KeyConstraint withUsing(String value) { + setUsing(value); + return this; + } + + public KeyConstraint withIndexSpec(List value) { + setIndexSpec(value); + return this; + } + + public KeyConstraint withSpecOptions(List value) { + setSpecOptions(value); + return this; + } + + public KeyConstraint withClustering(IndexOptions.Clustering value) { + setClustering(value); + return this; + } + + public KeyConstraint withType(String value) { + setType(value); + return this; + } + + public KeyConstraint withName(String value) { + setName(value); + return this; + } + + public KeyConstraint withName(List value) { + setName(value); + return this; + } + + @Override + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { + String spec = PlainSelect.getStringList(getIndexSpec(), false, false); + String keyword = indexKeyword != null && (getType() == null + || !getType().toUpperCase(java.util.Locale.ROOT) + .endsWith(indexKeyword.toUpperCase(java.util.Locale.ROOT))) + ? " " + indexKeyword + : ""; + appendConstraintPrefixTo(sql); + sql.append(getType()).append(nullsDistinctClause()).append(keyword) + .append(clusteringClause()); + if (indexName != null) { + sql.append(' ').append(indexName); + } + if (getUsing() != null) { + sql.append(" USING ").append(getUsing()); + } + if (columns != null) { + sql.append(' '); + appendColumnsTo(sql, expressionPrinter); + } + if (!spec.isEmpty()) { + sql.append(' ').append(spec); + } + appendIndexOptionsTo(sql, expressionPrinter); + appendConstraintSuffixTo(sql); + appendConstraintAttributesTo(sql); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/KeyElement.java b/src/main/java/net/sf/jsqlparser/statement/create/table/KeyElement.java new file mode 100644 index 0000000000..a0001681af --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/KeyElement.java @@ -0,0 +1,284 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** A column or expression key shared by index and constraint declarations. */ +public class KeyElement implements Serializable { + public enum SortOrder { + ASC, DESC + } + + public enum NullOrdering { + FIRST, LAST + } + + public final String columnName; + public final List params; + private final Expression expression; + private boolean expressionParenthesized = true; + private String collation; + private String operatorClass; + private List operatorClassParameters; + private SortOrder sortOrder; + private NullOrdering nullOrdering; + private ExclusionOperator exclusionOperator; + private boolean withoutOverlaps; + private boolean period; + + public String getExclusionOperator() { + return exclusionOperator == null ? null : exclusionOperator.toString(); + } + + /** Replaces the complete operator text; retained for source compatibility. */ + public void setExclusionOperator(String exclusionOperator) { + this.exclusionOperator = exclusionOperator == null ? null + : new ExclusionOperator().withName(exclusionOperator); + } + + /** Returns the editable operator reference, or null when this is not an exclusion key. */ + public ExclusionOperator getExclusionOperatorReference() { + return exclusionOperator; + } + + public void setExclusionOperatorReference(ExclusionOperator exclusionOperator) { + this.exclusionOperator = exclusionOperator; + } + + public KeyElement withExclusionOperatorReference(ExclusionOperator exclusionOperator) { + setExclusionOperatorReference(exclusionOperator); + return this; + } + + public KeyElement(String columnName) { + this.columnName = columnName; + this.params = null; + this.expression = null; + } + + public KeyElement(String columnName, List params) { + this.columnName = columnName; + this.params = params; + this.expression = null; + } + + public KeyElement(Expression expression) { + this.columnName = null; + this.params = null; + this.expression = expression; + } + + public KeyElement(Expression expression, List params) { + this.columnName = null; + this.params = params; + this.expression = expression; + } + + /** Marks the final key of a PostgreSQL temporal PRIMARY KEY or UNIQUE constraint. */ + public boolean isWithoutOverlaps() { + return withoutOverlaps; + } + + public void setWithoutOverlaps(boolean withoutOverlaps) { + this.withoutOverlaps = withoutOverlaps; + } + + public KeyElement withWithoutOverlaps(boolean withoutOverlaps) { + setWithoutOverlaps(withoutOverlaps); + return this; + } + + /** Marks the final referencing column of a temporal foreign key. */ + public boolean isPeriod() { + return period; + } + + public void setPeriod(boolean period) { + this.period = period; + } + + public KeyElement withPeriod(boolean period) { + setPeriod(period); + return this; + } + + public String getColumnName() { + return expression != null ? expression.toString() : columnName; + } + + public List getParams() { + return params; + } + + public Expression getExpression() { + return expression; + } + + public boolean isExpression() { + return expression != null; + } + + public boolean isExpressionParenthesized() { + return expressionParenthesized; + } + + public void setExpressionParenthesized(boolean expressionParenthesized) { + this.expressionParenthesized = expressionParenthesized; + } + + public KeyElement withExpressionParenthesized(boolean expressionParenthesized) { + setExpressionParenthesized(expressionParenthesized); + return this; + } + + public String getCollation() { + return collation; + } + + public void setCollation(String collation) { + this.collation = collation; + } + + public String getOperatorClass() { + return operatorClass; + } + + public void setOperatorClass(String operatorClass) { + this.operatorClass = operatorClass; + } + + public List getOperatorClassParameters() { + return operatorClassParameters; + } + + public void setOperatorClassParameters(List operatorClassParameters) { + this.operatorClassParameters = operatorClassParameters; + } + + public SortOrder getSortOrder() { + return sortOrder; + } + + public void setSortOrder(SortOrder sortOrder) { + this.sortOrder = sortOrder; + } + + public NullOrdering getNullOrdering() { + return nullOrdering; + } + + public void setNullOrdering(NullOrdering nullOrdering) { + this.nullOrdering = nullOrdering; + } + + public KeyElement withCollation(String collation) { + setCollation(collation); + return this; + } + + public KeyElement withOperatorClass(String operatorClass) { + setOperatorClass(operatorClass); + return this; + } + + public KeyElement withOperatorClassParameters(List operatorClassParameters) { + setOperatorClassParameters(operatorClassParameters); + return this; + } + + public KeyElement withSortOrder(SortOrder sortOrder) { + setSortOrder(sortOrder); + return this; + } + + public KeyElement withNullOrdering(NullOrdering nullOrdering) { + setNullOrdering(nullOrdering); + return this; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, value -> builder.append(value)); + return builder.toString(); + } + + /** Renders expression keys through the caller's expression printer. */ + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + if (period) { + builder.append("PERIOD "); + } + if (expression != null) { + if (expressionParenthesized) { + builder.append('('); + } + expressionPrinter.accept(expression); + if (expressionParenthesized) { + builder.append(')'); + } + } else { + builder.append(columnName); + } + appendParams(builder); + appendCollation(builder); + appendOperatorClass(builder, expressionPrinter); + appendSortOrder(builder); + appendNullOrdering(builder); + if (exclusionOperator != null) { + builder.append(" WITH ").append(exclusionOperator); + } + if (withoutOverlaps) { + builder.append(" WITHOUT OVERLAPS"); + } + } + + private void appendParams(StringBuilder builder) { + if (params != null) { + builder.append(" ").append(String.join(" ", params)); + } + } + + private void appendCollation(StringBuilder builder) { + if (collation != null && !hasParam("COLLATE")) { + builder.append(" COLLATE ").append(collation); + } + } + + private void appendOperatorClass(StringBuilder builder, + Consumer expressionPrinter) { + if (operatorClass != null && !hasParam(operatorClass)) { + builder.append(" ").append(operatorClass); + if (operatorClassParameters != null && !operatorClassParameters.isEmpty()) { + builder.append(" "); + IndexOption.appendListTo(builder, operatorClassParameters, expressionPrinter); + } + } + } + + private void appendSortOrder(StringBuilder builder) { + if (sortOrder != null && !hasParam(sortOrder.name())) { + builder.append(" ").append(sortOrder); + } + } + + private void appendNullOrdering(StringBuilder builder) { + if (nullOrdering != null && !hasParam("NULLS")) { + builder.append(" NULLS ").append(nullOrdering); + } + } + + private boolean hasParam(String expected) { + return params != null && params.stream().anyMatch(expected::equalsIgnoreCase); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java index ee2cce33bc..0e6e5bdc6e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java @@ -9,69 +9,153 @@ */ package net.sf.jsqlparser.statement.create.table; -import java.util.Collection; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.statement.select.PlainSelect; +/** Common syntax and attributes of a constraint; a constraint is not an index. */ +public abstract class NamedConstraint implements TableElement, Serializable { + public enum ConstraintNamePosition { + BEFORE, AFTER + } -public class NamedConstraint extends Index { + private final List name = new ArrayList<>(); + private String type; + private ConstraintKind kind = ConstraintKind.OTHER; + private ConstraintAttributes constraintAttributes; + private boolean useConstraintKeyword; + private ConstraintNamePosition constraintNamePosition = ConstraintNamePosition.BEFORE; - @Override - public String toString() { - String idxSpecText = PlainSelect.getStringList(getIndexSpec(), false, false); - String head = getName() != null ? "CONSTRAINT " + getName() + " " : ""; - String tail = getType() + " " + PlainSelect.getStringList(getColumnsNames(), true, true) + - (!"".equals(idxSpecText) ? " " + idxSpecText : ""); - return head + tail; + public String getName() { + return name.isEmpty() ? null : String.join(".", name); } - @Override - public NamedConstraint withName(List name) { - return (NamedConstraint) super.withName(name); + public void setName(String value) { + name.clear(); + if (value != null) { + name.add(value); + } } - @Override - public NamedConstraint withName(String name) { - return (NamedConstraint) super.withName(name); + public void setName(List value) { + name.clear(); + name.addAll(value); } - @Override - public NamedConstraint withType(String type) { - return (NamedConstraint) super.withType(type); + public List getNameParts() { + return Collections.unmodifiableList(name); } - @Override - public NamedConstraint withUsing(String using) { - return (NamedConstraint) super.withUsing(using); + public String getType() { + return type; } - @Override - public NamedConstraint withColumnsNames(List list) { - return (NamedConstraint) super.withColumnsNames(list); + public void setType(String value) { + type = value; + kind = ConstraintKind.fromType(value); } - @Override - public NamedConstraint withColumns(List columns) { - return (NamedConstraint) super.withColumns(columns); + public ConstraintKind getKind() { + return kind; } - @Override - public NamedConstraint addColumns(ColumnParams... functionDeclarationParts) { - return (NamedConstraint) super.addColumns(functionDeclarationParts); + public void setKind(ConstraintKind value) { + kind = value; } - @Override - public NamedConstraint addColumns(Collection functionDeclarationParts) { - return (NamedConstraint) super.addColumns(functionDeclarationParts); + public ConstraintAttributes getConstraintAttributes() { + return constraintAttributes; } - @Override - public NamedConstraint withIndexSpec(List idxSpec) { - return (NamedConstraint) super.withIndexSpec(idxSpec); + public void setConstraintAttributes(ConstraintAttributes value) { + constraintAttributes = value; + } + + public boolean isUseConstraintKeyword() { + return useConstraintKeyword; + } + + public void setUseConstraintKeyword(boolean value) { + useConstraintKeyword = value; + } + + public ConstraintNamePosition getConstraintNamePosition() { + return constraintNamePosition; + } + + public void setConstraintNamePosition(ConstraintNamePosition value) { + constraintNamePosition = Objects.requireNonNull(value, "constraintNamePosition"); + } + + public NamedConstraint withConstraintNamePosition(ConstraintNamePosition value) { + setConstraintNamePosition(value); + return this; + } + + public NamedConstraint withUseConstraintKeyword(boolean value) { + setUseConstraintKeyword(value); + return this; + } + + public NamedConstraint withKind(ConstraintKind value) { + setKind(value); + return this; + } + + public NamedConstraint withConstraintAttributes(ConstraintAttributes value) { + setConstraintAttributes(value); + return this; + } + + public NamedConstraint withType(String value) { + setType(value); + return this; + } + + public NamedConstraint withName(String value) { + setName(value); + return this; } + public NamedConstraint withName(List value) { + setName(value); + return this; + } + + public void appendConstraintPrefixTo(StringBuilder sql) { + boolean leadingName = + getName() != null && constraintNamePosition == ConstraintNamePosition.BEFORE; + if (useConstraintKeyword || leadingName) { + sql.append("CONSTRAINT"); + if (leadingName) { + sql.append(' ').append(getName()); + } + sql.append(' '); + } + } + + public void appendConstraintSuffixTo(StringBuilder sql) { + if (constraintNamePosition == ConstraintNamePosition.AFTER && getName() != null) { + sql.append(" CONSTRAINT ").append(getName()); + } + } + + public void appendConstraintAttributesTo(StringBuilder sql) { + if (constraintAttributes != null) { + constraintAttributes.appendTo(sql); + } + } + + public abstract void appendTo(StringBuilder sql, Consumer expressionPrinter); + @Override - public NamedConstraint withIndexKeyword(String indexKeyword) { - return (NamedConstraint) super.withIndexKeyword(indexKeyword); + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/NotNullConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/NotNullConstraint.java new file mode 100644 index 0000000000..df427b0d2d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/NotNullConstraint.java @@ -0,0 +1,98 @@ +/*- + * #%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.statement.create.table; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; + +/** PostgreSQL 18 column- or table-level NOT NULL constraint. */ +public class NotNullConstraint extends NamedConstraint { + private Column column; + private boolean noInherit; + private boolean columnConstraint; + + public NotNullConstraint() { + setType("NOT NULL"); + } + + @Override + public ConstraintKind getKind() { + return ConstraintKind.NOT_NULL; + } + + /** Whether the target is implicit in the enclosing column definition. */ + public boolean isColumnConstraint() { + return columnConstraint; + } + + public void setColumnConstraint(boolean columnConstraint) { + this.columnConstraint = columnConstraint; + } + + public NotNullConstraint withColumnConstraint(boolean columnConstraint) { + setColumnConstraint(columnConstraint); + return this; + } + + public Column getColumn() { + return column; + } + + public void setColumn(Column column) { + this.column = column; + } + + public boolean isNoInherit() { + return noInherit; + } + + public void setNoInherit(boolean noInherit) { + this.noInherit = noInherit; + } + + public NotNullConstraint withColumn(Column column) { + setColumn(column); + return this; + } + + public NotNullConstraint withNoInherit(boolean noInherit) { + setNoInherit(noInherit); + return this; + } + + @Override + public NotNullConstraint withName(String name) { + setName(name); + return this; + } + + public NotNullConstraint withConstraintAttributes(ConstraintAttributes attributes) { + setConstraintAttributes(attributes); + return this; + } + + @Override + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { + if (!columnConstraint && column == null) { + throw new IllegalStateException("NOT NULL requires a target column"); + } + appendConstraintPrefixTo(sql); + sql.append("NOT NULL"); + if (!columnConstraint) { + sql.append(' '); + expressionPrinter.accept(column); + } + if (noInherit) { + sql.append(" NO INHERIT"); + } + appendConstraintAttributesTo(sql); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java new file mode 100644 index 0000000000..0fe2a24ed0 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java @@ -0,0 +1,215 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** A PostgreSQL declarative-partition bound. */ +public class PartitionBound implements Serializable { + + public enum Type { + RANGE, LIST, HASH, DEFAULT + } + + private Type type; + private ExpressionList fromExpressions; + private ExpressionList toExpressions; + private ExpressionList inExpressions; + private Expression modulus; + private Expression remainder; + + public PartitionBound() {} + + public PartitionBound(Type type) { + this.type = type; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public ExpressionList getFromExpressions() { + return fromExpressions; + } + + public void setFromExpressions(ExpressionList fromExpressions) { + this.fromExpressions = fromExpressions; + } + + public ExpressionList getToExpressions() { + return toExpressions; + } + + public void setToExpressions(ExpressionList toExpressions) { + this.toExpressions = toExpressions; + } + + public ExpressionList getInExpressions() { + return inExpressions; + } + + public void setInExpressions(ExpressionList inExpressions) { + this.inExpressions = inExpressions; + } + + public Expression getModulus() { + return modulus; + } + + public void setModulus(Expression modulus) { + this.modulus = modulus; + } + + public Expression getRemainder() { + return remainder; + } + + public void setRemainder(Expression remainder) { + this.remainder = remainder; + } + + public PartitionBound withType(Type type) { + setType(type); + return this; + } + + public PartitionBound withFromExpressions(ExpressionList fromExpressions) { + setFromExpressions(fromExpressions); + return this; + } + + public PartitionBound withToExpressions(ExpressionList toExpressions) { + setToExpressions(toExpressions); + return this; + } + + public PartitionBound withInExpressions(ExpressionList inExpressions) { + setInExpressions(inExpressions); + return this; + } + + public PartitionBound withModulus(Expression modulus) { + setModulus(modulus); + return this; + } + + public PartitionBound withRemainder(Expression remainder) { + setRemainder(remainder); + return this; + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } + + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { + switch (type) { + case RANGE: + sql.append("FOR VALUES FROM ("); + appendRangeValues(sql, fromExpressions, expressionPrinter); + sql.append(") TO ("); + appendRangeValues(sql, toExpressions, expressionPrinter); + sql.append(')'); + break; + case LIST: + sql.append("FOR VALUES IN ("); + expressionPrinter.accept(inExpressions); + sql.append(')'); + break; + case HASH: + sql.append("FOR VALUES WITH (MODULUS "); + expressionPrinter.accept(modulus); + sql.append(", REMAINDER "); + expressionPrinter.accept(remainder); + sql.append(')'); + break; + case DEFAULT: + sql.append("DEFAULT"); + break; + default: + break; + } + } + + /** Visits active bound expressions, excluding the MINVALUE/MAXVALUE range markers. */ + public void visitExpressions(Consumer expressions) { + if (type == null) { + return; + } + switch (type) { + case RANGE: + visitRangeValues(fromExpressions, expressions); + visitRangeValues(toExpressions, expressions); + break; + case LIST: + if (inExpressions != null) { + expressions.accept(inExpressions); + } + break; + case HASH: + if (modulus != null) { + expressions.accept(modulus); + } + if (remainder != null) { + expressions.accept(remainder); + } + break; + default: + break; + } + } + + private static void visitRangeValues(ExpressionList values, + Consumer expressions) { + if (values != null) { + values.stream().filter(value -> !isRangeMarker(value)).forEach(expressions); + } + } + + private static void appendRangeValues(StringBuilder sql, ExpressionList values, + Consumer expressions) { + if (values == null) { + sql.append("null"); + return; + } + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sql.append(", "); + } + Expression value = values.get(i); + if (isRangeMarker(value)) { + sql.append(value); + } else { + expressions.accept(value); + } + } + } + + private static boolean isRangeMarker(Expression expression) { + if (!(expression instanceof Column)) { + return false; + } + Column column = (Column) expression; + return column.getTable() == null && ("MINVALUE".equalsIgnoreCase(column.getColumnName()) + || "MAXVALUE".equalsIgnoreCase(column.getColumnName())); + } + +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java index 24d15dfd04..3d764b3740 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java @@ -11,18 +11,55 @@ import java.io.Serializable; import java.util.List; +import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.statement.select.PlainSelect; public class PartitionDefinition implements Serializable { + + public enum ValueOperator { + LESS_THAN("VALUES LESS THAN"), IN("VALUES IN"); + + private final String sql; + + ValueOperator(String sql) { + this.sql = sql; + } + + @Override + public String toString() { + return sql; + } + } + private String partitionName; private String partitionOperation; private List values; private String storageEngine; + private ValueOperator valueOperator; + private boolean maxValue; + private boolean subPartition; + private boolean storageKeyword; + private boolean storageEngineUseEquals = true; + private StringValue commentValue; + private boolean commentUseEquals; + private String dataDirectory; + private boolean dataDirectoryUseEquals; + private String indexDirectory; + private boolean indexDirectoryUseEquals; + private String maxRows; + private boolean maxRowsUseEquals; + private String minRows; + private boolean minRowsUseEquals; + private String tablespace; + private boolean tablespaceUseEquals; + private List subPartitionDefinitions; + + public PartitionDefinition() {} public PartitionDefinition(String partitionName, String partitionOperation, List values, String storageEngine) { this.partitionName = partitionName; - this.partitionOperation = partitionOperation; + setPartitionOperation(partitionOperation); this.values = values; this.storageEngine = storageEngine; } @@ -41,6 +78,13 @@ public String getPartitionOperation() { public void setPartitionOperation(String partitionOperation) { this.partitionOperation = partitionOperation; + if (ValueOperator.LESS_THAN.toString().equalsIgnoreCase(partitionOperation)) { + valueOperator = ValueOperator.LESS_THAN; + } else if (ValueOperator.IN.toString().equalsIgnoreCase(partitionOperation)) { + valueOperator = ValueOperator.IN; + } else { + valueOperator = null; + } } public List getValues() { @@ -59,16 +103,200 @@ public void setStorageEngine(String storageEngine) { this.storageEngine = storageEngine; } + public ValueOperator getValueOperator() { + return valueOperator; + } + + public void setValueOperator(ValueOperator valueOperator) { + this.valueOperator = valueOperator; + this.partitionOperation = valueOperator != null ? valueOperator.toString() : null; + } + + public boolean isMaxValue() { + return maxValue; + } + + public void setMaxValue(boolean maxValue) { + this.maxValue = maxValue; + } + + public boolean isSubPartition() { + return subPartition; + } + + public void setSubPartition(boolean subPartition) { + this.subPartition = subPartition; + } + + public boolean isStorageKeyword() { + return storageKeyword; + } + + public void setStorageKeyword(boolean storageKeyword) { + this.storageKeyword = storageKeyword; + } + + public boolean isStorageEngineUseEquals() { + return storageEngineUseEquals; + } + + public void setStorageEngineUseEquals(boolean storageEngineUseEquals) { + this.storageEngineUseEquals = storageEngineUseEquals; + } + + /** Returns the COMMENT literal's SQL text, for example {@code 'it''s'}, or null. */ + public String getComment() { + return commentValue != null ? commentValue.toString() : null; + } + + /** + * Sets the COMMENT from a string literal's SQL text, read with standard escaping. Use + * {@link #setCommentValue(StringValue)} for another escaping. Null removes the comment. + */ + public void setComment(String comment) { + commentValue = comment != null ? new StringValue(comment) : null; + } + + /** Returns the COMMENT string literal; {@link StringValue#getNotExcapedValue()} decodes it. */ + public StringValue getCommentValue() { + return commentValue; + } + + public void setCommentValue(StringValue commentValue) { + this.commentValue = commentValue; + } + + public boolean isCommentUseEquals() { + return commentUseEquals; + } + + public void setCommentUseEquals(boolean commentUseEquals) { + this.commentUseEquals = commentUseEquals; + } + + public String getDataDirectory() { + return dataDirectory; + } + + public void setDataDirectory(String dataDirectory) { + this.dataDirectory = dataDirectory; + } + + public boolean isDataDirectoryUseEquals() { + return dataDirectoryUseEquals; + } + + public void setDataDirectoryUseEquals(boolean dataDirectoryUseEquals) { + this.dataDirectoryUseEquals = dataDirectoryUseEquals; + } + + public String getIndexDirectory() { + return indexDirectory; + } + + public void setIndexDirectory(String indexDirectory) { + this.indexDirectory = indexDirectory; + } + + public boolean isIndexDirectoryUseEquals() { + return indexDirectoryUseEquals; + } + + public void setIndexDirectoryUseEquals(boolean indexDirectoryUseEquals) { + this.indexDirectoryUseEquals = indexDirectoryUseEquals; + } + + public String getMaxRows() { + return maxRows; + } + + public void setMaxRows(String maxRows) { + this.maxRows = maxRows; + } + + public boolean isMaxRowsUseEquals() { + return maxRowsUseEquals; + } + + public void setMaxRowsUseEquals(boolean maxRowsUseEquals) { + this.maxRowsUseEquals = maxRowsUseEquals; + } + + public String getMinRows() { + return minRows; + } + + public void setMinRows(String minRows) { + this.minRows = minRows; + } + + public boolean isMinRowsUseEquals() { + return minRowsUseEquals; + } + + public void setMinRowsUseEquals(boolean minRowsUseEquals) { + this.minRowsUseEquals = minRowsUseEquals; + } + + public String getTablespace() { + return tablespace; + } + + public void setTablespace(String tablespace) { + this.tablespace = tablespace; + } + + public boolean isTablespaceUseEquals() { + return tablespaceUseEquals; + } + + public void setTablespaceUseEquals(boolean tablespaceUseEquals) { + this.tablespaceUseEquals = tablespaceUseEquals; + } + + public List getSubPartitionDefinitions() { + return subPartitionDefinitions; + } + + public void setSubPartitionDefinitions( + List subPartitionDefinitions) { + this.subPartitionDefinitions = subPartitionDefinitions; + } + @Override public String toString() { StringBuilder b = new StringBuilder(); - b.append("PARTITION ").append(partitionName) - .append(" ").append(partitionOperation) - .append(" (").append(PlainSelect.getStringList(values)) - .append(")"); + b.append(subPartition ? "SUBPARTITION " : "PARTITION ").append(partitionName); + if (partitionOperation != null) { + b.append(" ").append(partitionOperation); + if (maxValue) { + b.append(" MAXVALUE"); + } else { + b.append(" (").append(PlainSelect.getStringList(values)).append(")"); + } + } if (storageEngine != null) { - b.append(" ENGINE = ").append(storageEngine); + if (storageKeyword) { + b.append(" STORAGE"); + } + appendOption(b, "ENGINE", storageEngine, storageEngineUseEquals); + } + appendOption(b, "COMMENT", getComment(), commentUseEquals); + appendOption(b, "DATA DIRECTORY", dataDirectory, dataDirectoryUseEquals); + appendOption(b, "INDEX DIRECTORY", indexDirectory, indexDirectoryUseEquals); + appendOption(b, "MAX_ROWS", maxRows, maxRowsUseEquals); + appendOption(b, "MIN_ROWS", minRows, minRowsUseEquals); + appendOption(b, "TABLESPACE", tablespace, tablespaceUseEquals); + if (subPartitionDefinitions != null && !subPartitionDefinitions.isEmpty()) { + b.append(" ").append(PlainSelect.getStringList(subPartitionDefinitions, true, true)); } return b.toString(); } + + private static void appendOption(StringBuilder builder, String name, String value, + boolean useEquals) { + if (value != null) { + builder.append(" ").append(name).append(useEquals ? " = " : " ").append(value); + } + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TableElement.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableElement.java new file mode 100644 index 0000000000..be533ae569 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TableElement.java @@ -0,0 +1,22 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; + +/** + * A column or table constraint/index declared inside a {@code CREATE TABLE} definition. + * + *

+ * This common type lets callers inspect columns, constraints and indexes in their source order + * without merging the separate typed lists themselves. + */ +public interface TableElement extends Serializable { +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TableElementList.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableElementList.java new file mode 100644 index 0000000000..c221e986d0 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TableElementList.java @@ -0,0 +1,140 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** A mutable, filtered view preserving the other declarations in a table definition. */ +final class TableElementList extends AbstractList + implements Serializable { + private final List elements; + private final Class type; + + TableElementList(List elements, Class type) { + this.elements = elements; + this.type = type; + } + + @Override + public int size() { + int count = 0; + for (TableElement element : elements) { + if (type.isInstance(element)) { + count++; + } + } + return count; + } + + @Override + public Iterator iterator() { + return new Iterator() { + private int cursor; + private int last = -1; + + @Override + public boolean hasNext() { + while (cursor < elements.size() && !type.isInstance(elements.get(cursor))) { + cursor++; + } + return cursor < elements.size(); + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + last = cursor++; + return type.cast(elements.get(last)); + } + + @Override + public void remove() { + if (last < 0) { + throw new IllegalStateException(); + } + elements.remove(last); + cursor--; + last = -1; + modCount++; + } + }; + } + + private int elementIndex(int index, boolean insertion) { + if (index < 0) { + throw new IndexOutOfBoundsException(Integer.toString(index)); + } + int count = 0; + int end = elements.size(); + for (int i = 0; i < elements.size(); i++) { + if (type.isInstance(elements.get(i))) { + if (count++ == index) { + return i; + } + end = i + 1; + } + } + if (insertion && index == count) { + return end; + } + throw new IndexOutOfBoundsException(Integer.toString(index)); + } + + @Override + public E get(int index) { + return type.cast(elements.get(elementIndex(index, false))); + } + + @Override + public E set(int index, E element) { + return type.cast(elements.set(elementIndex(index, false), Objects.requireNonNull(element))); + } + + @Override + public void add(int index, E element) { + elements.add(elementIndex(index, true), Objects.requireNonNull(element)); + modCount++; + } + + @Override + public E remove(int index) { + E removed = type.cast(elements.remove(elementIndex(index, false))); + modCount++; + return removed; + } + + static void replace(List elements, Class type, + List replacements) { + // The replacement may itself be a view of elements. + Iterator replacement = (replacements == null ? Collections.emptyList() + : new ArrayList<>(replacements)).iterator(); + ListIterator iterator = elements.listIterator(); + while (iterator.hasNext()) { + if (type.isInstance(iterator.next())) { + if (replacement.hasNext()) { + iterator.set(replacement.next()); + } else { + iterator.remove(); + } + } + } + replacement.forEachRemaining(iterator::add); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java new file mode 100644 index 0000000000..d895f1f040 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java @@ -0,0 +1,385 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.schema.Table; + +/** + * A shared CREATE/ALTER TABLE option with either opaque tokens or structured properties. Setting + * non-null tokens discards all structured properties. A structured setter discards the tokens; + * their contents are never parsed or restored by a later setter. MySQL tablespace statements reuse + * this model for their options; {@link Kind#FILE_BLOCK_SIZE} occurs only there. + */ +public class TableOption implements Serializable { + + private ForeignTableOptions foreignTableOptions; + private Kind kind = Kind.OTHER; + private String name; + private String value; + private boolean useEquals; + private List tokens; + private List

unionTables; + private List storageParameters; + private ColumnOption.Storage tablespaceStorage; + private OnCommitAction onCommitAction; + private StringValue comment; + + public enum Kind { + ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, SECONDARY_ENGINE, AUTOEXTEND_SIZE, INSERT_METHOD, PACK_KEYS, DELAY_KEY_WRITE, CHECKSUM, CONNECTION, COMPRESSION, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, ROW_FORMAT, AVG_ROW_LENGTH, MAX_ROWS, MIN_ROWS, KEY_BLOCK_SIZE, TABLESPACE, FOREIGN_SERVER, ACCESS_METHOD, ON_COMMIT, FILE_BLOCK_SIZE, OTHER + } + + /** + * The action of a temporary table's {@code ON COMMIT} clause. The DEFINITION actions belong to + * Oracle private temporary tables. + */ + public enum OnCommitAction { + PRESERVE_ROWS, DELETE_ROWS, DROP, PRESERVE_DEFINITION, DROP_DEFINITION; + + /** Returns the SQL keywords, for example {@code DELETE ROWS}. */ + @Override + public String toString() { + return name().replace('_', ' '); + } + } + + public ForeignTableOptions getForeignTableOptions() { + return foreignTableOptions; + } + + /** Replaces the current contents with a structured PostgreSQL SERVER clause. */ + public void setForeignTableOptions(ForeignTableOptions options) { + clearContents(); + kind = Kind.FOREIGN_SERVER; + name = "SERVER"; + foreignTableOptions = options; + } + + public static TableOption foreignServer(ForeignTableOptions options) { + TableOption option = new TableOption(); + option.setForeignTableOptions(options); + return option; + } + + public ColumnOption.Storage getTablespaceStorage() { + return tablespaceStorage; + } + + /** + * Sets the TABLESPACE storage modifier. A non-null modifier selects TABLESPACE and discards + * another option's contents; an existing TABLESPACE keeps its name, value and equals spelling. + */ + public void setTablespaceStorage(ColumnOption.Storage storage) { + if (storage != null && storage != ColumnOption.Storage.DISK + && storage != ColumnOption.Storage.MEMORY) { + throw new IllegalArgumentException("TABLESPACE storage must be DISK or MEMORY"); + } + if (storage != null && kind != Kind.TABLESPACE) { + clearContents(); + kind = Kind.TABLESPACE; + name = "TABLESPACE"; + } + tokens = null; + tablespaceStorage = storage; + } + + /** Creates a table access method option such as PostgreSQL's {@code USING heap}. */ + public static TableOption accessMethod(String method) { + return new TableOption(Kind.ACCESS_METHOD, "USING", method, false); + } + + /** Returns the {@code ON COMMIT} action, or null if this option has none. */ + public OnCommitAction getOnCommitAction() { + return onCommitAction; + } + + /** Replaces the current contents with an {@code ON COMMIT} clause. */ + public void setOnCommitAction(OnCommitAction action) { + clearContents(); + kind = Kind.ON_COMMIT; + name = "ON COMMIT"; + onCommitAction = action; + } + + /** Creates an {@code ON COMMIT} option, for example {@code ON COMMIT DROP}. */ + public static TableOption onCommit(OnCommitAction action) { + TableOption option = new TableOption(); + option.setOnCommitAction(action); + return option; + } + + public List getStorageParameters() { + return storageParameters; + } + + public void setStorageParameters(List storageParameters) { + clearContents(); + this.storageParameters = storageParameters; + kind = Kind.STORAGE_PARAMETERS; + name = "WITH"; + } + + /** + * Returns the COMMENT string literal, or null for another kind, opaque tokens or a plain value + * set with {@link #setValue(String)}. {@link StringValue#getNotExcapedValue()} decodes it. + */ + public StringValue getComment() { + return comment; + } + + /** + * Replaces the current contents with a COMMENT string literal and keeps the equals spelling; an + * existing COMMENT also keeps its name. A null literal omits the value. + */ + public void setComment(StringValue comment) { + String commentName = kind == Kind.COMMENT && name != null ? name : "COMMENT"; + boolean equals = useEquals; + clearContents(); + kind = Kind.COMMENT; + name = commentName; + useEquals = equals; + this.comment = comment; + } + + public TableOption withComment(StringValue comment) { + setComment(comment); + return this; + } + + public static TableOption comment(StringValue comment) { + TableOption option = new TableOption(); + option.setComment(comment); + return option; + } + + public void appendTo(StringBuilder builder, + java.util.function.Consumer expressionPrinter) { + if (foreignTableOptions != null) { + foreignTableOptions.appendTo(builder, expressionPrinter); + } else if (storageParameters != null) { + builder.append(name).append(useEquals ? " = " : " "); + IndexOption.appendListTo(builder, storageParameters, expressionPrinter); + } else if (comment != null) { + builder.append(name).append(useEquals ? " = " : " "); + expressionPrinter.accept(comment); + } else { + builder.append(toString()); + } + } + + + public TableOption() {} + + public TableOption(Kind kind, String name, String value, boolean useEquals) { + this.kind = kind; + this.name = name; + this.value = value; + this.useEquals = useEquals; + } + + public static TableOption raw(List tokens) { + TableOption option = new TableOption(); + option.setTokens(tokens); + return option; + } + + public static TableOption raw(String... tokens) { + return raw(Arrays.asList(tokens)); + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + tokens = null; + this.kind = kind; + if (kind != Kind.TABLESPACE) { + tablespaceStorage = null; + } + if (kind != Kind.FOREIGN_SERVER) { + foreignTableOptions = null; + } + if (kind != Kind.STORAGE_PARAMETERS) { + storageParameters = null; + } + if (kind != Kind.UNION) { + unionTables = null; + } + if (kind != Kind.ON_COMMIT) { + onCommitAction = null; + } + if (kind != Kind.COMMENT && comment != null) { + // keep the literal's SQL text, as for other scalar values + value = comment.toString(); + comment = null; + } + } + + public String getName() { + return name; + } + + public void setName(String name) { + tokens = null; + this.name = name; + } + + public String getValue() { + if (foreignTableOptions != null) { + return foreignTableOptions.toString().substring("SERVER ".length()); + } + if (storageParameters != null) { + return PlainSelect.getStringList(storageParameters, true, true); + } + if (onCommitAction != null) { + return onCommitAction.toString(); + } + if (comment != null) { + return comment.toString(); + } + return unionTables == null ? value : PlainSelect.getStringList(unionTables, true, true); + } + + /** Sets a plain value; it replaces a COMMENT literal without being parsed. */ + public void setValue(String value) { + tokens = null; + this.value = value; + foreignTableOptions = null; + unionTables = null; + storageParameters = null; + onCommitAction = null; + comment = null; + } + + /** Returns the mutable MERGE table sources, including an empty UNION list. */ + public List
getUnionTables() { + return unionTables; + } + + /** Replaces raw option contents with structured UNION table references. */ + public void setUnionTables(List
unionTables) { + boolean equals = useEquals; + clearContents(); + this.unionTables = unionTables; + kind = Kind.UNION; + name = "UNION"; + useEquals = equals; + } + + public TableOption withUnionTables(List
unionTables) { + setUnionTables(unionTables); + return this; + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + tokens = null; + this.useEquals = useEquals; + } + + /** + * Returns the original, mutable token list for an opaque option, or an unmodifiable snapshot of + * the current structured properties. Editing tokens does not classify or parse them. + */ + public List getTokens() { + if (tokens != null) { + return tokens; + } + List result = new ArrayList<>(); + if (name != null) { + Collections.addAll(result, name.trim().split("\\s+")); + } + if (useEquals) { + result.add("="); + } + String renderedValue = getValue(); + if (onCommitAction != null) { + // one token per keyword, as in the legacy raw options + Collections.addAll(result, renderedValue.split(" ")); + } else if (renderedValue != null) { + result.add(renderedValue); + } + if (kind == Kind.TABLESPACE && tablespaceStorage != null) { + result.add("STORAGE"); + result.add(tablespaceStorage.name()); + } + return Collections.unmodifiableList(result); + } + + /** + * Replaces all structured properties with opaque tokens and {@link Kind#OTHER}. Null clears + * opaque tokens without restoring discarded properties; it leaves structured options alone. + */ + public void setTokens(List tokens) { + if (tokens != null) { + clearContents(); + } + this.tokens = tokens; + } + + private void clearContents() { + kind = Kind.OTHER; + name = null; + value = null; + useEquals = false; + tokens = null; + tablespaceStorage = null; + foreignTableOptions = null; + unionTables = null; + storageParameters = null; + onCommitAction = null; + comment = null; + } + + public TableOption withKind(Kind kind) { + setKind(kind); + return this; + } + + public TableOption withName(String name) { + setName(name); + return this; + } + + public TableOption withValue(String value) { + setValue(value); + return this; + } + + public TableOption withUseEquals(boolean useEquals) { + setUseEquals(useEquals); + return this; + } + + @Override + public String toString() { + if (foreignTableOptions != null) { + return foreignTableOptions.toString(); + } + if (tokens != null) { + return PlainSelect.getStringList(tokens, false, false); + } + String renderedValue = getValue(); + return name + (renderedValue != null ? (useEquals ? " = " : " ") + renderedValue : "") + + (kind == Kind.TABLESPACE && tablespaceStorage != null + ? " STORAGE " + tablespaceStorage + : ""); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java new file mode 100644 index 0000000000..fa99c656a4 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java @@ -0,0 +1,362 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** + * MySQL table partitioning options used by {@code CREATE TABLE} and {@code ALTER TABLE}. + */ +public class TablePartitioning implements Serializable { + + public enum Type { + HASH, KEY, RANGE, LIST, + /** + * BigQuery partitions by a bare expression, without a partitioning method keyword: + * {@code PARTITION BY DATE(ts)}. + */ + EXPRESSION + } + + private List keyColumns; + + /** PostgreSQL keys carrying collation/operator-class attributes, otherwise null. */ + public List getKeyColumns() { + return keyColumns; + } + + public void setKeyColumns(List keyColumns) { + if (keyColumns != null) { + clearKeyRepresentation(); + } + this.keyColumns = keyColumns; + } + + private Type type; + private boolean linear; + private boolean columnsSyntax; + private Expression expression; + private ExpressionList expressionList; + private ExpressionList columns; + private Integer algorithm; + private boolean algorithmUseEquals; + private Long partitions; + private TablePartitioning subPartitioning; + private List partitionDefinitions; + private List partitionOptions; + + public TablePartitioning() {} + + public TablePartitioning(Type type) { + this.type = type; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public boolean isLinear() { + return linear; + } + + public void setLinear(boolean linear) { + this.linear = linear; + } + + public boolean isColumnsSyntax() { + return columnsSyntax; + } + + public void setColumnsSyntax(boolean columnsSyntax) { + this.columnsSyntax = columnsSyntax; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + if (expression != null) { + clearKeyRepresentation(); + } + this.expression = expression; + } + + /** + * Returns the ordered partition key expressions when more than one key was declared. + * + * @return partition key expressions, or {@code null} for the legacy single-expression form + */ + public ExpressionList getExpressionList() { + return expressionList; + } + + public void setExpressionList(ExpressionList expressionList) { + if (expressionList != null) { + clearKeyRepresentation(); + } + this.expressionList = expressionList; + } + + public ExpressionList getColumns() { + return columns; + } + + public void setColumns(ExpressionList columns) { + if (columns != null) { + clearKeyRepresentation(); + } + this.columns = columns; + } + + /** Selecting a key representation must replace the previously rendered key. */ + private void clearKeyRepresentation() { + keyColumns = null; + expression = null; + expressionList = null; + columns = null; + } + + public Integer getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(Integer algorithm) { + this.algorithm = algorithm; + } + + public boolean isAlgorithmUseEquals() { + return algorithmUseEquals; + } + + public void setAlgorithmUseEquals(boolean algorithmUseEquals) { + this.algorithmUseEquals = algorithmUseEquals; + } + + public Long getPartitions() { + return partitions; + } + + public void setPartitions(Long partitions) { + this.partitions = partitions; + } + + public TablePartitioning getSubPartitioning() { + return subPartitioning; + } + + public void setSubPartitioning(TablePartitioning subPartitioning) { + this.subPartitioning = subPartitioning; + } + + public List getPartitionDefinitions() { + return partitionDefinitions; + } + + public void setPartitionDefinitions(List partitionDefinitions) { + this.partitionDefinitions = partitionDefinitions; + } + + public List getPartitionOptions() { + return partitionOptions; + } + + public void setPartitionOptions(List partitionOptions) { + this.partitionOptions = partitionOptions; + } + + public TablePartitioning withType(Type type) { + setType(type); + return this; + } + + public TablePartitioning withLinear(boolean linear) { + setLinear(linear); + return this; + } + + public TablePartitioning withColumnsSyntax(boolean columnsSyntax) { + setColumnsSyntax(columnsSyntax); + return this; + } + + public TablePartitioning withExpression(Expression expression) { + setExpression(expression); + return this; + } + + public TablePartitioning withExpressionList(ExpressionList expressionList) { + setExpressionList(expressionList); + return this; + } + + public TablePartitioning withColumns(ExpressionList columns) { + setColumns(columns); + return this; + } + + public TablePartitioning withAlgorithm(Integer algorithm) { + setAlgorithm(algorithm); + return this; + } + + public TablePartitioning withAlgorithmUseEquals(boolean algorithmUseEquals) { + setAlgorithmUseEquals(algorithmUseEquals); + return this; + } + + public TablePartitioning withPartitions(Long partitions) { + setPartitions(partitions); + return this; + } + + public TablePartitioning withSubPartitioning(TablePartitioning subPartitioning) { + setSubPartitioning(subPartitioning); + return this; + } + + public TablePartitioning withPartitionDefinitions( + List partitionDefinitions) { + setPartitionDefinitions(partitionDefinitions); + return this; + } + + public TablePartitioning withPartitionOptions(List partitionOptions) { + setPartitionOptions(partitionOptions); + return this; + } + + public TablePartitioning addColumns(Column... columns) { + ExpressionList collection = + Optional.ofNullable(getColumns()).orElseGet(ExpressionList::new); + Collections.addAll(collection, columns); + return withColumns(collection); + } + + public TablePartitioning addColumns(Collection columns) { + ExpressionList collection = + Optional.ofNullable(getColumns()).orElseGet(ExpressionList::new); + collection.addAll(columns); + return withColumns(collection); + } + + public TablePartitioning addPartitionDefinitions( + PartitionDefinition... partitionDefinitions) { + List collection = Optional.ofNullable(getPartitionDefinitions()) + .orElseGet(ArrayList::new); + Collections.addAll(collection, partitionDefinitions); + return withPartitionDefinitions(collection); + } + + public TablePartitioning addPartitionDefinitions( + Collection partitionDefinitions) { + List collection = Optional.ofNullable(getPartitionDefinitions()) + .orElseGet(ArrayList::new); + collection.addAll(partitionDefinitions); + return withPartitionDefinitions(collection); + } + + public TablePartitioning addPartitionOptions(String... partitionOptions) { + List collection = + Optional.ofNullable(getPartitionOptions()).orElseGet(ArrayList::new); + Collections.addAll(collection, partitionOptions); + return withPartitionOptions(collection); + } + + public TablePartitioning addPartitionOptions(Collection partitionOptions) { + List collection = + Optional.ofNullable(getPartitionOptions()).orElseGet(ArrayList::new); + collection.addAll(partitionOptions); + return withPartitionOptions(collection); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } + + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append("PARTITION BY "); + appendMethod(builder, expressionPrinter); + if (partitions != null) { + builder.append(" PARTITIONS ").append(partitions); + } + if (subPartitioning != null) { + builder.append(" SUBPARTITION BY "); + subPartitioning.appendMethod(builder, expressionPrinter); + if (subPartitioning.getPartitions() != null) { + builder.append(" SUBPARTITIONS ").append(subPartitioning.getPartitions()); + } + } + if (partitionDefinitions != null && !partitionDefinitions.isEmpty()) { + builder.append(" ") + .append(PlainSelect.getStringList(partitionDefinitions, true, true)); + } + if (partitionOptions != null && !partitionOptions.isEmpty()) { + builder.append(" ").append(PlainSelect.getStringList(partitionOptions, false, false)); + } + } + + private void appendMethod(StringBuilder builder, Consumer expressionPrinter) { + if (type == Type.EXPRESSION) { + expressionPrinter.accept(expression); + return; + } + if (linear) { + builder.append("LINEAR "); + } + builder.append(type); + if (algorithm != null) { + builder.append(" ALGORITHM"); + builder.append(algorithmUseEquals ? " = " : " ").append(algorithm); + } + if (columnsSyntax) { + builder.append(" COLUMNS"); + } + if (keyColumns != null) { + builder.append(" ("); + for (int i = 0; i < keyColumns.size(); i++) { + if (i > 0) { + builder.append(", "); + } + keyColumns.get(i).appendTo(builder, expressionPrinter); + } + builder.append(')'); + } else if (expression != null) { + builder.append(" ("); + expressionPrinter.accept(expression); + builder.append(')'); + } else if (expressionList != null) { + builder.append(" ("); + expressionPrinter.accept(expressionList); + builder.append(')'); + } else if (columns != null) { + builder.append(" ("); + expressionPrinter.accept(columns); + builder.append(')'); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/XmlTypeModifier.java b/src/main/java/net/sf/jsqlparser/statement/create/table/XmlTypeModifier.java new file mode 100644 index 0000000000..61d0a7af82 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/XmlTypeModifier.java @@ -0,0 +1,72 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** A SQL Server XML schema collection reference, with an optional CONTENT/DOCUMENT facet. */ +public final class XmlTypeModifier implements Serializable { + public enum Kind { + CONTENT, DOCUMENT + } + + private final Kind kind; + private final List schemaCollection; + + public XmlTypeModifier(Kind kind, List schemaCollection) { + if (schemaCollection == null || schemaCollection.isEmpty() || schemaCollection.size() > 2 + || schemaCollection.stream().anyMatch(Objects::isNull)) { + throw new IllegalArgumentException( + "An XML schema collection needs a name and optional schema"); + } + this.kind = kind; + this.schemaCollection = Collections.unmodifiableList(new ArrayList<>(schemaCollection)); + } + + /** Null preserves an omitted facet, which SQL Server interprets as CONTENT. */ + public Kind getKind() { + return kind; + } + + public Kind getEffectiveKind() { + return kind == null ? Kind.CONTENT : kind; + } + + /** Name parts retain their original identifier quoting. */ + public List getSchemaCollection() { + return schemaCollection; + } + + @Override + public String toString() { + return "(" + (kind == null ? "" : kind + " ") + String.join(".", schemaCollection) + ")"; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof XmlTypeModifier)) { + return false; + } + XmlTypeModifier that = (XmlTypeModifier) other; + return kind == that.kind && schemaCollection.equals(that.schemaCollection); + } + + @Override + public int hashCode() { + return Objects.hash(kind, schemaCollection); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/tablespace/CreateTablespace.java b/src/main/java/net/sf/jsqlparser/statement/create/tablespace/CreateTablespace.java new file mode 100644 index 0000000000..45901d661e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/tablespace/CreateTablespace.java @@ -0,0 +1,160 @@ +/*- + * #%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.statement.create.tablespace; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.TableOption; + +/** + * A MySQL {@code CREATE [UNDO] TABLESPACE} or PostgreSQL {@code CREATE TABLESPACE} statement. The + * parser fills either the PostgreSQL owner, location and parameters, or the MySQL data file and + * options. MySQL options reuse the {@link TableOption} models of the same name, plus + * {@link TableOption.Kind#FILE_BLOCK_SIZE}. + */ +public class CreateTablespace extends ASTNodeAccessImpl implements Statement { + private String name; + private boolean undo; + private String owner; + private StringValue location; + private List parameters; + private StringValue dataFile; + private final List options = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** Whether this is a MySQL {@code CREATE UNDO TABLESPACE} statement. */ + public boolean isUndo() { + return undo; + } + + public void setUndo(boolean undo) { + this.undo = undo; + } + + /** Returns the PostgreSQL {@code OWNER} role, or null when the clause is omitted. */ + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + /** Returns the PostgreSQL {@code LOCATION} directory. */ + public StringValue getLocation() { + return location; + } + + public void setLocation(StringValue location) { + this.location = location; + } + + /** + * Returns the PostgreSQL {@code WITH (...)} tablespace parameters, or null when the clause is + * omitted. + */ + public List getParameters() { + return parameters; + } + + public void setParameters(List parameters) { + this.parameters = parameters; + } + + /** Returns the MySQL {@code ADD DATAFILE} file name, or null when the clause is omitted. */ + public StringValue getDataFile() { + return dataFile; + } + + public void setDataFile(StringValue dataFile) { + this.dataFile = dataFile; + } + + /** Returns the mutable MySQL options in source order; commas between them are not kept. */ + public List getOptions() { + return options; + } + + public void setOptions(Collection options) { + this.options.clear(); + if (options != null) { + this.options.addAll(options); + } + } + + public CreateTablespace addOptions(TableOption... options) { + Collections.addAll(this.options, options); + return this; + } + + /** Returns the first MySQL option of the given kind. */ + public Optional getOption(TableOption.Kind kind) { + return options.stream().filter(option -> option.getKind() == kind).findFirst(); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append(undo ? "CREATE UNDO TABLESPACE " : "CREATE TABLESPACE ").append(name); + if (owner != null) { + builder.append(" OWNER ").append(owner); + } + if (location != null) { + builder.append(" LOCATION ").append(location); + } + if (parameters != null) { + builder.append(" WITH "); + IndexOption.appendListTo(builder, parameters, expressionPrinter); + } + if (dataFile != null) { + builder.append(" ADD DATAFILE ").append(dataFile); + } + for (TableOption option : options) { + builder.append(' '); + option.appendTo(builder, expressionPrinter); + } + return builder; + } + + /** Visits the values of the PostgreSQL tablespace parameters. */ + public void visitExpressions(Consumer visitor) { + if (parameters != null) { + parameters.stream().map(IndexOption::getValue).filter(Objects::nonNull) + .forEach(visitor); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/textsearch/CreateTextSearchConfiguration.java b/src/main/java/net/sf/jsqlparser/statement/create/textsearch/CreateTextSearchConfiguration.java new file mode 100644 index 0000000000..ae214768bd --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/textsearch/CreateTextSearchConfiguration.java @@ -0,0 +1,63 @@ +/*- + * #%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.statement.create.textsearch; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class CreateTextSearchConfiguration extends ASTNodeAccessImpl implements Statement { + public enum SourceKind { + PARSER, COPY + } + + private String name; + private SourceKind sourceKind; + private String sourceName; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public SourceKind getSourceKind() { + return sourceKind; + } + + public void setSourceKind(SourceKind value) { + sourceKind = value; + } + + public String getSourceName() { + return sourceName; + } + + public void setSourceName(String value) { + sourceName = value; + } + + public StringBuilder appendTo(StringBuilder sql) { + return sql.append("CREATE TEXT SEARCH CONFIGURATION ").append(name) + .append(" (").append(sourceKind).append(" = ").append(sourceName).append(')'); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateEventTrigger.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateEventTrigger.java new file mode 100644 index 0000000000..13f0e5b09a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateEventTrigger.java @@ -0,0 +1,197 @@ +/*- + * #%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.statement.create.trigger; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * A PostgreSQL {@code CREATE EVENT TRIGGER} statement. Unlike {@link CreateTrigger}, it fires on a + * database event such as {@code ddl_command_start} rather than on table rows. The function is + * invoked like a {@link CreateTrigger} routine, with the same {@code FUNCTION} or {@code PROCEDURE} + * spelling, but never takes arguments. + */ +public class CreateEventTrigger extends ASTNodeAccessImpl implements Statement { + + private String name; + private String event; + private final List filters = new ArrayList<>(); + private CreateTrigger.ExecuteKeyword executeKeyword = CreateTrigger.ExecuteKeyword.FUNCTION; + private Function function; + + /** A {@code WHEN} condition, for example {@code TAG IN ('CREATE TABLE', 'DROP TABLE')}. */ + public static class Filter implements Serializable { + private String variable; + private final List values = new ArrayList<>(); + + public Filter() { + // empty constructor + } + + public Filter(String variable, Collection values) { + this.variable = variable; + setValues(values); + } + + /** The filter variable as written, for example {@code TAG}. */ + public String getVariable() { + return variable; + } + + public void setVariable(String variable) { + this.variable = variable; + } + + /** The accepted values in source order. */ + public List getValues() { + return Collections.unmodifiableList(values); + } + + public void setValues(Collection values) { + this.values.clear(); + if (values != null) { + this.values.addAll(values); + } + } + + public Filter addValues(StringValue... values) { + Collections.addAll(this.values, values); + return this; + } + + public StringBuilder appendTo(StringBuilder sql, Consumer expressionWriter) { + sql.append(variable).append(" IN ("); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sql.append(", "); + } + expressionWriter.accept(values.get(i)); + } + return sql.append(')'); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append).toString(); + } + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** The event name as written, for example {@code ddl_command_start} or {@code sql_drop}. */ + public String getEvent() { + return event; + } + + public void setEvent(String event) { + this.event = event; + } + + /** The {@code WHEN} conditions in source order; all of them must match. */ + public List getFilters() { + return Collections.unmodifiableList(filters); + } + + public void setFilters(Collection filters) { + this.filters.clear(); + if (filters != null) { + this.filters.addAll(filters); + } + } + + public CreateEventTrigger addFilters(Filter... filters) { + Collections.addAll(this.filters, filters); + return this; + } + + public CreateTrigger.ExecuteKeyword getExecuteKeyword() { + return executeKeyword; + } + + public void setExecuteKeyword(CreateTrigger.ExecuteKeyword executeKeyword) { + this.executeKeyword = executeKeyword; + } + + /** The invoked function, rendered with its empty argument list. */ + public Function getFunction() { + return function; + } + + public void setFunction(Function function) { + this.function = function; + } + + /** Visits the filter values and the invoked function. */ + public void visitExpressions(Consumer visitor) { + filters.forEach(filter -> filter.getValues().forEach(visitor)); + if (function != null) { + visitor.accept(function); + } + } + + public StringBuilder appendTo(StringBuilder sql, Consumer expressionWriter) { + sql.append("CREATE EVENT TRIGGER ").append(name).append(" ON ").append(event); + for (int i = 0; i < filters.size(); i++) { + sql.append(i == 0 ? " WHEN " : " AND "); + filters.get(i).appendTo(sql, expressionWriter); + } + sql.append(" EXECUTE ").append(executeKeyword).append(' '); + expressionWriter.accept(function); + return sql; + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append).toString(); + } + + public CreateEventTrigger withName(String name) { + setName(name); + return this; + } + + public CreateEventTrigger withEvent(String event) { + setEvent(event); + return this; + } + + public CreateEventTrigger withExecuteKeyword(CreateTrigger.ExecuteKeyword executeKeyword) { + setExecuteKeyword(executeKeyword); + return this; + } + + public CreateEventTrigger withFunction(Function function) { + setFunction(function); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java new file mode 100644 index 0000000000..bcda2c8f59 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java @@ -0,0 +1,350 @@ +/*- + * #%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.statement.create.trigger; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** MySQL and PostgreSQL trigger definitions with distinct body and routine-invocation forms. */ +public class CreateTrigger extends ASTNodeAccessImpl implements Statement { + + public enum Timing { + BEFORE, AFTER, INSTEAD_OF + } + + public enum Event { + INSERT, UPDATE, DELETE, TRUNCATE + } + + public enum Order { + FOLLOWS, PRECEDES + } + + public enum Orientation { + ROW, STATEMENT + } + public enum ExecuteKeyword { + FUNCTION, PROCEDURE + } + + private List events = new ArrayList<>(); + private TriggerDefiner definer; + private Table trigger; + private Timing timing; + private boolean orReplace; + private boolean ifNotExists; + private boolean constraint; + private Table referencedTable; + private Orientation orientation; + private boolean useEach; + private Expression whenExpression; + private ExecuteKeyword executeKeyword = ExecuteKeyword.FUNCTION; + private Function routine; + private Table table; + private Order order; + private Table otherTrigger; + private Statement body; + private ConstraintAttributes constraintAttributes = new ConstraintAttributes(); + private List transitionRelations = new ArrayList<>(); + + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events; + } + + public boolean isOrReplace() { + return orReplace; + } + + public void setOrReplace(boolean orReplace) { + this.orReplace = orReplace; + } + + /** + * Whether MySQL's {@code IF NOT EXISTS} follows {@code TRIGGER}. + * + * @return {@code true} if {@code IF NOT EXISTS} is rendered + */ + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + public boolean isConstraint() { + return constraint; + } + + public void setConstraint(boolean constraint) { + this.constraint = constraint; + } + + public Table getReferencedTable() { + return referencedTable; + } + + public void setReferencedTable(Table referencedTable) { + this.referencedTable = referencedTable; + } + + public ConstraintAttributes getConstraintAttributes() { + return constraintAttributes; + } + + public void setConstraintAttributes(ConstraintAttributes constraintAttributes) { + this.constraintAttributes = constraintAttributes; + } + + public List getTransitionRelations() { + return transitionRelations; + } + + public void setTransitionRelations(List transitionRelations) { + this.transitionRelations = transitionRelations; + } + + public Orientation getOrientation() { + return orientation; + } + + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + } + + public boolean isUseEach() { + return useEach; + } + + public void setUseEach(boolean useEach) { + this.useEach = useEach; + } + + public Expression getWhenExpression() { + return whenExpression; + } + + public void setWhenExpression(Expression whenExpression) { + this.whenExpression = whenExpression; + } + + public ExecuteKeyword getExecuteKeyword() { + return executeKeyword; + } + + public void setExecuteKeyword(ExecuteKeyword executeKeyword) { + this.executeKeyword = executeKeyword; + } + + public Function getRoutine() { + return routine; + } + + public void setRoutine(Function routine) { + this.routine = routine; + } + + public TriggerDefiner getDefiner() { + return definer; + } + + public void setDefiner(TriggerDefiner definer) { + this.definer = definer; + } + + public Table getTrigger() { + return trigger; + } + + public void setTrigger(Table trigger) { + this.trigger = trigger; + } + + public Timing getTiming() { + return timing; + } + + public void setTiming(Timing timing) { + this.timing = timing; + } + + public Event getEvent() { + return events.isEmpty() ? null : events.get(0).getEvent(); + } + + public void setEvent(Event event) { + events = new ArrayList<>(); + if (event != null) { + events.add(new TriggerEvent(event)); + } + } + + public Table getTable() { + return table; + } + + public void setTable(Table table) { + this.table = table; + } + + public Order getOrder() { + return order; + } + + public void setOrder(Order order) { + this.order = order; + } + + public Table getOtherTrigger() { + return otherTrigger; + } + + public void setOtherTrigger(Table otherTrigger) { + this.otherTrigger = otherTrigger; + } + + /** + * The MySQL trigger statement: a single statement, a {@link net.sf.jsqlparser.statement.Block} + * of statements, or an {@link net.sf.jsqlparser.statement.UnsupportedStatement} holding the + * tokens of a compound statement with procedural statements, such as {@code IF} or + * {@code DECLARE}. + * + * @return the body + */ + public Statement getBody() { + return body; + } + + public void setBody(Statement body) { + this.body = body; + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + public void visit(Consumer
tables, Consumer expressions) { + if (table != null) { + tables.accept(table); + } + if (referencedTable != null) { + tables.accept(referencedTable); + } + for (TriggerEvent triggerEvent : events) { + if (triggerEvent.getColumns() != null) { + triggerEvent.getColumns().forEach(expressions); + } + } + if (whenExpression != null) { + expressions.accept(whenExpression); + } + if (routine != null) { + expressions.accept(routine); + } + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + // MySQL's shorter form has neither a routine nor any of the standard clauses + if (routine == null && transitionRelations.isEmpty() && !constraint + && orientation != Orientation.STATEMENT) { + sql.append(mysqlSql()); + return; + } + appendPostgreSqlHeader(sql, visitor); + appendPostgreSqlTarget(sql); + if (whenExpression != null) { + sql.append(" WHEN ("); + visitor.accept(whenExpression); + sql.append(')'); + } + if (routine != null) { + sql.append(" EXECUTE ").append(executeKeyword).append(' '); + visitor.accept(routine); + } else { + // DuckDB 2.0 runs a statement rather than a function + sql.append(' ').append(body); + } + } + + private void appendPostgreSqlHeader(StringBuilder sql, Consumer visitor) { + sql.append("CREATE "); + if (orReplace) { + sql.append("OR REPLACE "); + } + if (constraint) { + sql.append("CONSTRAINT "); + } + sql.append("TRIGGER ").append(trigger).append(' ').append(timing.name().replace('_', ' ')) + .append(' '); + for (int i = 0; i < events.size(); i++) { + if (i > 0) { + sql.append(" OR "); + } + events.get(i).appendTo(sql, visitor); + } + } + + private void appendPostgreSqlTarget(StringBuilder sql) { + sql.append(" ON ").append(table); + if (referencedTable != null) { + sql.append(" FROM ").append(referencedTable); + } + constraintAttributes.appendTo(sql); + if (!transitionRelations.isEmpty()) { + sql.append(" REFERENCING"); + for (TransitionRelation relation : transitionRelations) { + sql.append(' ').append(relation); + } + } + if (orientation != null) { + sql.append(" FOR ").append(useEach ? "EACH " : "").append(orientation); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } + + private String mysqlSql() { + StringBuilder builder = new StringBuilder("CREATE "); + if (orReplace) { + builder.append("OR REPLACE "); + } + if (definer != null) { + builder.append("DEFINER = ").append(definer).append(" "); + } + builder.append("TRIGGER "); + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + builder.append(trigger).append(" ").append(timing).append(" ") + .append(getEvent()).append(" ON ").append(table).append(" FOR EACH ROW "); + if (order != null) { + builder.append(order).append(" ").append(otherTrigger).append(" "); + } + return builder.append(body).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/TransitionRelation.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TransitionRelation.java new file mode 100644 index 0000000000..6f5eb77c5e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TransitionRelation.java @@ -0,0 +1,51 @@ +/*- + * #%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.statement.create.trigger; + +import java.io.Serializable; + +public class TransitionRelation implements Serializable { + public enum Image { + OLD, NEW + } + + private Image image; + private String name; + private boolean useAs; + + public Image getImage() { + return image; + } + + public void setImage(Image image) { + this.image = image; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isUseAs() { + return useAs; + } + + public void setUseAs(boolean useAs) { + this.useAs = useAs; + } + + @Override + public String toString() { + return image + " TABLE " + (useAs ? "AS " : "") + name; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerDefiner.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerDefiner.java new file mode 100644 index 0000000000..9f95c53ea5 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerDefiner.java @@ -0,0 +1,28 @@ +/*- + * #%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.statement.create.trigger; + +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.MySqlDefiner; + +/** Retains the trigger API while sharing MySQL DEFINER account handling with views. */ +public class TriggerDefiner extends MySqlDefiner { + @Override + public TriggerDefiner withUser(StringValue user) { + setUser(user); + return this; + } + + @Override + public TriggerDefiner withHost(StringValue host) { + setHost(host); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerEvent.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerEvent.java new file mode 100644 index 0000000000..da7c35bff9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerEvent.java @@ -0,0 +1,61 @@ +/*- + * #%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.statement.create.trigger; + +import java.io.Serializable; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +public class TriggerEvent implements Serializable { + private CreateTrigger.Event event; + private ExpressionList columns; + + public CreateTrigger.Event getEvent() { + return event; + } + + public void setEvent(CreateTrigger.Event event) { + this.event = event; + } + + public ExpressionList getColumns() { + return columns; + } + + public void setColumns(ExpressionList columns) { + this.columns = columns; + } + + public TriggerEvent(CreateTrigger.Event event) { + this.event = event; + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + sql.append(event); + if (columns != null) { + sql.append(" OF "); + for (int i = 0; i < columns.size(); i++) { + if (i > 0) { + sql.append(", "); + } + visitor.accept(columns.get(i)); + } + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/type/CompositeTypeDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/type/CompositeTypeDefinition.java new file mode 100644 index 0000000000..ae785207ef --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/type/CompositeTypeDefinition.java @@ -0,0 +1,32 @@ +/*- + * #%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.statement.create.type; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class CompositeTypeDefinition implements TypeDefinition { + private List attributes = new ArrayList<>(); + + public List getAttributes() { + return attributes; + } + + public void setAttributes(List attributes) { + this.attributes = attributes; + } + + @Override + public String toString() { + return "(" + attributes.stream().map(Object::toString).collect(Collectors.joining(", ")) + + ")"; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/type/CreateType.java b/src/main/java/net/sf/jsqlparser/statement/create/type/CreateType.java new file mode 100644 index 0000000000..4d84bbd229 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/type/CreateType.java @@ -0,0 +1,46 @@ +/*- + * #%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.statement.create.type; + + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class CreateType extends ASTNodeAccessImpl implements Statement { + private String name; + private TypeDefinition definition; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TypeDefinition getDefinition() { + return definition; + } + + public void setDefinition(TypeDefinition definition) { + this.definition = definition; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public String toString() { + return "CREATE TYPE " + name + (definition == null ? "" : " AS " + definition); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/type/EnumTypeDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/type/EnumTypeDefinition.java new file mode 100644 index 0000000000..2cb3f062e1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/type/EnumTypeDefinition.java @@ -0,0 +1,33 @@ +/*- + * #%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.statement.create.type; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.expression.StringValue; + +public class EnumTypeDefinition implements TypeDefinition { + private List labels = new ArrayList<>(); + + public List getLabels() { + return labels; + } + + public void setLabels(List labels) { + this.labels = labels; + } + + @Override + public String toString() { + return "ENUM (" + labels.stream().map(Object::toString).collect(Collectors.joining(", ")) + + ")"; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/type/RangeTypeDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/type/RangeTypeDefinition.java new file mode 100644 index 0000000000..56ab0cdf1d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/type/RangeTypeDefinition.java @@ -0,0 +1,76 @@ +/*- + * #%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.statement.create.type; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.statement.create.table.ColDataType; + +public class RangeTypeDefinition implements TypeDefinition { + private List
tables; private List usingFromItemList; @@ -45,6 +49,7 @@ public class Delete implements Statement { private Expression where; private PreferringClause preferringClause; private Limit limit; + private OptionClause option; private List orderByElements; private boolean hasFrom = true; private DeleteModifierPriority modifierPriority; @@ -111,6 +116,19 @@ public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); } + public ForPortionClause getForPortionClause() { + return forPortionClause; + } + + public void setForPortionClause(ForPortionClause forPortionClause) { + this.forPortionClause = forPortionClause; + } + + public Delete withForPortionClause(ForPortionClause forPortionClause) { + setForPortionClause(forPortionClause); + return this; + } + public Table getTable() { return table; } @@ -147,6 +165,19 @@ public Limit getLimit() { return limit; } + public OptionClause getOption() { + return option; + } + + public Delete setOption(OptionClause option) { + this.option = option; + return this; + } + + public Delete withOption(OptionClause option) { + return setOption(option); + } + public void setLimit(Limit limit) { this.limit = limit; } @@ -246,6 +277,9 @@ public String toString() { b.append(" FROM"); } b.append(" ").append(table); + if (forPortionClause != null) { + b.append(' ').append(forPortionClause); + } if (usingFromItemList != null && !usingFromItemList.isEmpty()) { b.append(" USING "); @@ -280,6 +314,10 @@ public String toString() { b.append(limit); } + if (option != null) { + b.append(option); + } + if (returningClause != null) { returningClause.appendTo(b); } diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java index a7ac362535..2c29b8dc8a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java +++ b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java @@ -14,22 +14,84 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; -import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.RoutineReference; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.index.AlgorithmOrLockOption; +import net.sf.jsqlparser.statement.create.user.UserAccount; import net.sf.jsqlparser.statement.select.PlainSelect; -public class Drop implements Statement { +public class Drop extends ASTNodeAccessImpl implements Statement { + + public enum ObjectType { + DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, FOREIGN_DATA_WRAPPER, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, ACCESS_METHOD, LANGUAGE, OPERATOR_CLASS, OPERATOR_FAMILY, TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE, OTHER; + + /** Targets are {@link RoutineReference}s, each with an optional argument signature. */ + public boolean hasRoutineTargets() { + return this == FUNCTION || this == PROCEDURE || this == ROUTINE || this == AGGREGATE; + } + + /** Targets are role or MySQL account names, including the roles of DROP OWNED BY. */ + public boolean hasAccountTargets() { + return this == USER || this == ROLE || this == GROUP || this == OWNED; + } + + /** The target can name the table that owns it with ON. */ + public boolean hasOwnerTable() { + return this == INDEX || this == TRIGGER || this == RULE; + } + + /** The target is named within the index access method given with USING. */ + public boolean hasAccessMethod() { + return this == OPERATOR_CLASS || this == OPERATOR_FAMILY; + } + } private String type; - private Table name; + private ObjectType objectType = ObjectType.OTHER; + private final List
names = new ArrayList<>(); + private final List routines = new ArrayList<>(); + private final List accounts = new ArrayList<>(); private List parameters; + private Table table; + private int tablePosition; + private final List algorithmOrLockOptions = new ArrayList<>(); + private Integer algorithmOrLockPosition; + private CascadeBehavior behavior; private Map> typeToParameters = new HashMap<>(); private boolean ifExists = false; private boolean materialized = false; + private boolean concurrently; + private boolean undo; + private boolean force; + private boolean publicSynonym; + private boolean procedural; + private String accessMethod; + private String engine; + private boolean engineUseEquals; + + /** PostgreSQL DROP INDEX CONCURRENTLY, preceding IF EXISTS and the index name. */ + public boolean isConcurrently() { + return concurrently; + } + + public void setConcurrently(boolean concurrently) { + this.concurrently = concurrently; + } + + public Drop withConcurrently(boolean concurrently) { + setConcurrently(concurrently); + return this; + } + private boolean isUsingTemporary; @@ -46,19 +108,331 @@ public T accept(StatementVisitor statementVisitor, S context) { } public Table getName() { - return name; + List
current = getNames(); + return current.isEmpty() ? null : current.get(0); } - public void setName(Table string) { - name = string; + public void setName(Table name) { + useNameTargets(); + names.clear(); + if (name != null) { + names.add(name); + } + } + + /** + * Returns the dropped object names. Routine and account targets return a read-only snapshot of + * their names; edit {@link #getRoutines()} or {@link #getAccounts()} to change them. + */ + public List
getNames() { + if (!routines.isEmpty()) { + return Collections.unmodifiableList(routines.stream() + .map(routine -> routine.getName() == null ? new Table() + : qualifiedTable(routine.getName())) + .collect(Collectors.toList())); + } + if (!accounts.isEmpty()) { + return Collections.unmodifiableList(accounts.stream() + .map(account -> new Table(account.toString(), false)) + .collect(Collectors.toList())); + } + return Collections.unmodifiableList(names); + } + + public void setNames(Collection names) { + useNameTargets(); + this.names.clear(); + if (names != null) { + this.names.addAll(names); + } + } + + /** + * Routine targets of DROP FUNCTION, PROCEDURE, ROUTINE and AGGREGATE. A routine without an + * argument list has null arguments; an explicit {@code ()} has an empty list. + */ + public List getRoutines() { + return Collections.unmodifiableList(routines); + } + + /** Replaces the targets with routines, discarding names, accounts and legacy parameters. */ + public void setRoutines(Collection routines) { + clearTargets(); + if (routines != null) { + this.routines.addAll(routines); + } + } + + public Drop withRoutines(Collection routines) { + setRoutines(routines); + return this; + } + + /** Appends routine targets. Name or account targets are discarded first. */ + public Drop addRoutines(RoutineReference... routines) { + if (routines.length > 0 && this.routines.isEmpty()) { + clearTargets(); + } + Collections.addAll(this.routines, routines); + return this; + } + + /** Role or MySQL account targets of DROP USER, ROLE, GROUP and the roles of DROP OWNED BY. */ + public List getAccounts() { + return Collections.unmodifiableList(accounts); + } + + /** Replaces the targets with accounts, discarding names, routines and legacy parameters. */ + public void setAccounts(Collection accounts) { + clearTargets(); + if (accounts != null) { + this.accounts.addAll(accounts); + } + } + + public Drop withAccounts(Collection accounts) { + setAccounts(accounts); + return this; } + /** Appends account targets. Name or routine targets are discarded first. */ + public Drop addAccounts(UserAccount... accounts) { + if (accounts.length > 0 && this.accounts.isEmpty()) { + clearTargets(); + } + Collections.addAll(this.accounts, accounts); + return this; + } + + private void clearTargets() { + names.clear(); + routines.clear(); + accounts.clear(); + typeToParameters = new HashMap<>(); + } + + /** + * Converts routine or account targets to the legacy representation before a legacy edit: the + * names, and the last routine's argument declarations as the single legacy signature. + */ + private void useNameTargets() { + if (routines.isEmpty() && accounts.isEmpty()) { + return; + } + List
legacyNames = getNames(); + Map> legacyParameters = new HashMap<>(); + getTypeToParameters().forEach((key, value) -> legacyParameters.put(key, + value == null ? null : new ArrayList<>(value))); + routines.clear(); + accounts.clear(); + names.addAll(legacyNames); + typeToParameters = legacyParameters; + } + + /** Splits a routine name at the dots outside quoted identifiers. */ + private static Table qualifiedTable(String name) { + List parts = new ArrayList<>(); + int start = 0; + char closingQuote = 0; + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + if (closingQuote != 0) { + if (ch == closingQuote) { + closingQuote = 0; + } + } else if (ch == '"' || ch == '`') { + closingQuote = ch; + } else if (ch == '[') { + closingQuote = ']'; + } else if (ch == '.') { + parts.add(name.substring(start, i)); + start = i + 1; + } + } + parts.add(name.substring(start)); + return parts.size() == 1 ? new Table(parts.get(0), false) : new Table(parts); + } + + /** + * Returns the legacy tokens after the targets. With a structured ON table, ALGORITHM or LOCK + * clauses or CASCADE/RESTRICT behavior, this is a snapshot that includes them in source order; + * edit {@link #setTable}, {@link #getAlgorithmOrLockOptions()} or {@link #setBehavior} instead. + */ public List getParameters() { - return parameters; + if (table == null && algorithmOrLockOptions.isEmpty() && behavior == null) { + return parameters; + } + List tokens = new ArrayList<>(); + appendParameters(tokens::add, table -> tokens.add(table.toString())); + if (behavior != null) { + tokens.add(behavior.name()); + } + return tokens; + } + + /** + * The table named by ON in DROP INDEX, DROP TRIGGER or DROP RULE, or null when SQL does not + * name one. + */ + public Table getTable() { + return table; + } + + public void setTable(Table table) { + setTable(table, this.table == null ? 0 : tablePosition); + } + + /** Places ON among the remaining parameter tokens, preserving their source order. */ + public void setTable(Table table, int parameterPosition) { + int size = parameters == null ? 0 : parameters.size(); + if (parameterPosition < 0 || parameterPosition > size) { + throw new IllegalArgumentException("Invalid ON table position: " + parameterPosition); + } + this.table = table; + tablePosition = table == null ? 0 : parameterPosition; + } + + public Drop withTable(Table table) { + setTable(table); + return this; + } + + /** Visits table/view targets and explicit owner tables without resolving catalog objects. */ + public void visitTables(Consumer
visitor) { + if (objectType == ObjectType.TABLE || objectType == ObjectType.VIEW + || objectType == ObjectType.FOREIGN_TABLE) { + names.forEach(visitor); + } else if (objectType.hasOwnerTable() && table != null) { + visitor.accept(table); + } } + /** + * Replaces all legacy parameters and clears the structured ON table, the ALGORITHM and LOCK + * clauses and the CASCADE/RESTRICT behavior. + */ public void setParameters(List list) { parameters = list; + table = null; + tablePosition = 0; + algorithmOrLockOptions.clear(); + algorithmOrLockPosition = null; + behavior = null; + } + + /** The explicit CASCADE or RESTRICT clause, or null when it is omitted. */ + public CascadeBehavior getBehavior() { + return behavior; + } + + public void setBehavior(CascadeBehavior behavior) { + this.behavior = behavior; + } + + public Drop withBehavior(CascadeBehavior behavior) { + setBehavior(behavior); + return this; + } + + /** MySQL DROP UNDO TABLESPACE. */ + public boolean isUndo() { + return undo; + } + + public void setUndo(boolean undo) { + this.undo = undo; + } + + public Drop withUndo(boolean undo) { + setUndo(undo); + return this; + } + + /** PostgreSQL DROP DATABASE ... WITH (FORCE), or Oracle DROP SYNONYM ... FORCE. */ + public boolean isForce() { + return force; + } + + public void setForce(boolean force) { + this.force = force; + } + + public Drop withForce(boolean force) { + setForce(force); + return this; + } + + /** Whether the synonym belongs to the PUBLIC namespace. */ + public boolean isPublicSynonym() { + return publicSynonym; + } + + public void setPublicSynonym(boolean publicSynonym) { + this.publicSynonym = publicSynonym; + } + + public Drop withPublicSynonym(boolean publicSynonym) { + setPublicSynonym(publicSynonym); + return this; + } + + /** Whether a {@link ObjectType#LANGUAGE} is written as PostgreSQL PROCEDURAL LANGUAGE. */ + public boolean isProcedural() { + return procedural; + } + + public void setProcedural(boolean procedural) { + this.procedural = procedural; + } + + public Drop withProcedural(boolean procedural) { + setProcedural(procedural); + return this; + } + + /** + * The index access method of an {@link ObjectType#OPERATOR_CLASS} or + * {@link ObjectType#OPERATOR_FAMILY}, written with USING. + */ + public String getAccessMethod() { + return accessMethod; + } + + public void setAccessMethod(String accessMethod) { + this.accessMethod = accessMethod; + } + + public Drop withAccessMethod(String accessMethod) { + setAccessMethod(accessMethod); + return this; + } + + /** The storage engine of MySQL DROP TABLESPACE ... ENGINE, or null when it is omitted. */ + public String getEngine() { + return engine; + } + + public void setEngine(String engine) { + this.engine = engine; + } + + public Drop withEngine(String engine) { + setEngine(engine); + return this; + } + + /** Whether the ENGINE clause is written with an equals sign. */ + public boolean isEngineUseEquals() { + return engineUseEquals; + } + + public void setEngineUseEquals(boolean engineUseEquals) { + this.engineUseEquals = engineUseEquals; + } + + public Drop withEngineUseEquals(boolean engineUseEquals) { + setEngineUseEquals(engineUseEquals); + return this; } public String getType() { @@ -67,6 +441,22 @@ public String getType() { public void setType(String string) { type = string; + try { + objectType = ObjectType.valueOf(string.toUpperCase(Locale.ROOT).replace(' ', '_')); + } catch (IllegalArgumentException | NullPointerException ignored) { + objectType = ObjectType.OTHER; + } + } + + public ObjectType getObjectType() { + return objectType; + } + + public void setObjectType(ObjectType objectType) { + this.objectType = objectType == null ? ObjectType.OTHER : objectType; + if (this.objectType != ObjectType.OTHER) { + this.type = this.objectType.name().replace('_', ' '); + } } public boolean isIfExists() { @@ -98,35 +488,133 @@ public void setMaterialized(boolean materialized) { this.materialized = materialized; } + /** + * Returns the legacy routine parameters by object type. For routine targets, this is a + * read-only snapshot of the last routine's argument declarations, where the legacy + * single-signature form kept them; edit {@link #getRoutines()} instead. + */ public Map> getTypeToParameters() { - return typeToParameters; + if (routines.isEmpty()) { + return typeToParameters; + } + RoutineReference last = routines.get(routines.size() - 1); + List arguments = null; + if (last.isAllArguments()) { + arguments = Collections.singletonList("*"); + } else if (last.getArguments() != null) { + arguments = Collections.unmodifiableList(last.getArguments().stream() + .map(Object::toString).collect(Collectors.toList())); + } + return Collections.singletonMap( + objectType.hasRoutineTargets() ? objectType.name() : type, arguments); } public void setTypeToParameters(Map> typeToParameters) { + useNameTargets(); this.typeToParameters = typeToParameters; } @Override public String toString() { - String sql = "DROP " - + (isUsingTemporary ? "TEMPORARY " : "") - + (materialized ? "MATERIALIZED " : "") - + type + " " - + (ifExists ? "IF EXISTS " : "") + name.toString(); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer
tablePrinter) { + builder.append("DROP "); + if (isUsingTemporary) { + builder.append("TEMPORARY "); + } + if (materialized) { + builder.append("MATERIALIZED "); + } + if (undo) { + builder.append("UNDO "); + } + if (publicSynonym) { + builder.append("PUBLIC "); + } + if (procedural) { + builder.append("PROCEDURAL "); + } + builder.append(type); + if (concurrently) { + builder.append(" CONCURRENTLY"); + } + if (objectType == ObjectType.OWNED) { + builder.append(" BY"); + } + builder.append(ifExists ? " IF EXISTS " : " "); + appendTargets(builder, tablePrinter); + if (accessMethod != null) { + builder.append(" USING ").append(accessMethod); + } + if (force) { + builder.append(objectType == ObjectType.SYNONYM ? " FORCE" : " WITH (FORCE)"); + } + if (engine != null) { + builder.append(engineUseEquals ? " ENGINE = " : " ENGINE ").append(engine); + } + appendParameters(token -> builder.append(' ').append(token), table -> { + builder.append(' '); + tablePrinter.accept(table); + }); + if (behavior != null) { + builder.append(' ').append(behavior); + } + return builder; + } - if (type.equals("FUNCTION")) { - sql += formatFuncParams(getParamsByType("FUNCTION")); + private void appendTargets(StringBuilder builder, Consumer
tablePrinter) { + if (!routines.isEmpty()) { + builder.append(routines.stream().map(Object::toString) + .collect(Collectors.joining(", "))); + } else if (!accounts.isEmpty()) { + builder.append(accounts.stream().map(Object::toString) + .collect(Collectors.joining(", "))); + } else { + appendNames(builder, tablePrinter); + if ("FUNCTION".equals(type)) { + builder.append(formatFuncParams(getParamsByType("FUNCTION"))); + } } + } - if (parameters != null && !parameters.isEmpty()) { - sql += " " + PlainSelect.getStringList(parameters, false, false); + private void appendNames(StringBuilder builder, Consumer
tablePrinter) { + for (int i = 0; i < names.size(); i++) { + if (i > 0) { + builder.append(", "); + } + if (objectType == ObjectType.TABLE || objectType == ObjectType.VIEW + || objectType == ObjectType.FOREIGN_TABLE) { + tablePrinter.accept(names.get(i)); + } else { + builder.append(names.get(i)); + } } + } - return sql; + /** Emits the parameter tokens with ON and the ALGORITHM/LOCK clauses at their positions. */ + private void appendParameters(Consumer tokens, Consumer
onTable) { + int size = parameters == null ? 0 : parameters.size(); + int optionPosition = algorithmOrLockPosition == null ? size + : Math.min(algorithmOrLockPosition, size); + for (int i = 0; i <= size; i++) { + if (table != null && i == tablePosition) { + tokens.accept("ON"); + onTable.accept(table); + } + if (i == optionPosition) { + algorithmOrLockOptions.forEach(option -> option.getTokens().forEach(tokens)); + } + if (i < size) { + tokens.accept(parameters.get(i)); + } + } } public List getParamsByType(String type) { - return typeToParameters.get(type); + return getTypeToParameters().get(type); } public Drop withIfExists(boolean ifExists) { @@ -149,20 +637,79 @@ public Drop withName(Table name) { return this; } + public Drop withNames(Collection names) { + setNames(names); + return this; + } + + public Drop addNames(Table... names) { + useNameTargets(); + Collections.addAll(this.names, names); + return this; + } + + public Drop withObjectType(ObjectType objectType) { + setObjectType(objectType); + return this; + } + public Drop withParameters(List parameters) { this.setParameters(parameters); return this; } public Drop addParameters(String... parameters) { - List collection = Optional.ofNullable(getParameters()).orElseGet(ArrayList::new); - Collections.addAll(collection, parameters); - return this.withParameters(collection); + return addParameters(java.util.Arrays.asList(parameters)); } + /** Appends trailing tokens without discarding a structured ON table or behavior. */ public Drop addParameters(Collection parameters) { - List collection = Optional.ofNullable(getParameters()).orElseGet(ArrayList::new); - collection.addAll(parameters); - return this.withParameters(collection); + if (this.parameters == null) { + this.parameters = new ArrayList<>(); + } + this.parameters.addAll(parameters); + return this; + } + + /** + * Returns the mutable MySQL ALGORITHM and LOCK clauses of DROP INDEX in source order. They + * follow the parameter tokens unless positioned with + * {@link #setAlgorithmOrLockOptions(List, int)}. + */ + public List getAlgorithmOrLockOptions() { + return algorithmOrLockOptions; + } + + /** Copies the list membership and places the clauses after all parameter tokens. */ + public void setAlgorithmOrLockOptions(List options) { + setAlgorithmOrLockOptions(options, -1); + } + + /** + * Copies the list membership and places the clauses before the parameter token at + * {@code parameterPosition}; -1 places them after all parameter tokens. + */ + public void setAlgorithmOrLockOptions(List options, + int parameterPosition) { + int size = parameters == null ? 0 : parameters.size(); + if (parameterPosition < -1 || parameterPosition > size) { + throw new IllegalArgumentException( + "Invalid ALGORITHM/LOCK position: " + parameterPosition); + } + algorithmOrLockOptions.clear(); + if (options != null) { + algorithmOrLockOptions.addAll(options); + } + algorithmOrLockPosition = parameterPosition < 0 ? null : parameterPosition; + } + + public Drop withAlgorithmOrLockOptions(List options) { + setAlgorithmOrLockOptions(options); + return this; + } + + public Drop addAlgorithmOrLockOptions(AlgorithmOrLockOption... options) { + Collections.addAll(algorithmOrLockOptions, options); + return this; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/DropPolicy.java b/src/main/java/net/sf/jsqlparser/statement/drop/DropPolicy.java new file mode 100644 index 0000000000..c276d7e4fc --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/drop/DropPolicy.java @@ -0,0 +1,80 @@ +/*- + * #%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.statement.drop; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class DropPolicy extends ASTNodeAccessImpl implements Statement { + private String policyName; + private net.sf.jsqlparser.schema.Table table; + private boolean ifExists = false; + private CascadeBehavior behavior; + + public String getPolicyName() { + return policyName; + } + + public DropPolicy setPolicyName(String policyName) { + this.policyName = policyName; + return this; + } + + public net.sf.jsqlparser.schema.Table getTable() { + return table; + } + + public DropPolicy setTable(net.sf.jsqlparser.schema.Table table) { + this.table = table; + return this; + } + + public boolean isIfExists() { + return ifExists; + } + + public DropPolicy setIfExists(boolean ifExists) { + this.ifExists = ifExists; + return this; + } + + public CascadeBehavior getBehavior() { + return behavior; + } + + public DropPolicy setBehavior(CascadeBehavior behavior) { + this.behavior = behavior; + return this; + } + + public StringBuilder appendTo(StringBuilder sql) { + sql.append("DROP POLICY "); + if (ifExists) { + sql.append("IF EXISTS "); + } + sql.append(policyName).append(" ON ").append(table); + if (behavior != null) { + sql.append(' ').append(behavior); + } + return sql; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/DropUserMapping.java b/src/main/java/net/sf/jsqlparser/statement/drop/DropUserMapping.java new file mode 100644 index 0000000000..a0324e94b8 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/drop/DropUserMapping.java @@ -0,0 +1,66 @@ +/*- + * #%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.statement.drop; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL {@code DROP USER MAPPING [IF EXISTS] FOR user SERVER server}. A user mapping has no + * name of its own, so it is identified by the user and the server, as in + * {@link net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping}. + */ +public class DropUserMapping implements Statement { + private String user; + private String server; + private boolean ifExists; + + public String getUser() { + return user; + } + + public void setUser(String value) { + user = value; + } + + public String getServer() { + return server; + } + + public void setServer(String value) { + server = value; + } + + public boolean isIfExists() { + return ifExists; + } + + public void setIfExists(boolean value) { + ifExists = value; + } + + public StringBuilder appendTo(StringBuilder sql) { + sql.append("DROP USER MAPPING "); + if (ifExists) { + sql.append("IF EXISTS "); + } + return sql.append("FOR ").append(user).append(" SERVER ").append(server); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/execute/Execute.java b/src/main/java/net/sf/jsqlparser/statement/execute/Execute.java index f29ecb2169..cde107b66e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/execute/Execute.java +++ b/src/main/java/net/sf/jsqlparser/statement/execute/Execute.java @@ -10,15 +10,18 @@ package net.sf.jsqlparser.statement.execute; import java.util.Locale; +import java.util.StringJoiner; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; -import net.sf.jsqlparser.statement.select.PlainSelect; import java.util.List; -public class Execute implements Statement { +public class Execute extends ASTNodeAccessImpl implements Statement { private ExecType execType = ExecType.EXECUTE; private String name; @@ -33,13 +36,11 @@ public void setName(String name) { } public void setName(List names) { + StringJoiner qualifiedName = new StringJoiner("."); for (String item : names) { - if (this.name != null) { - this.name = this.name + "." + item; - } else { - this.name = item; - } + qualifiedName.add(item == null ? "" : item); } + this.name = qualifiedName.toString(); } public ExpressionList getExprList() { @@ -70,11 +71,32 @@ public T accept(StatementVisitor statementVisitor, S context) { @Override public String toString() { - return execType.name() + " " + name - + (exprList != null - ? " " + PlainSelect.getStringList(exprList, true, - exprList instanceof ParenthesedExpressionList) - : ""); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + if (execType != ExecType.IMPLICIT) { + builder.append(execType.name()).append(' '); + } + builder.append(name); + if (exprList != null) { + builder.append(' '); + boolean brackets = exprList instanceof ParenthesedExpressionList; + if (brackets) { + builder.append('('); + } + for (int i = 0; i < exprList.size(); i++) { + if (i > 0) { + builder.append(", "); + } + expressionPrinter.accept((Expression) exprList.get(i)); + } + if (brackets) { + builder.append(')'); + } + } + return builder; } public Execute withExecType(ExecType execType) { @@ -93,7 +115,7 @@ public Execute withExprList(ExpressionList exprList) { } public enum ExecType { - EXECUTE, EXEC, CALL; + EXECUTE, EXEC, CALL, IMPLICIT; public static ExecType from(String type) { return Enum.valueOf(ExecType.class, type.toUpperCase(Locale.ROOT)); diff --git a/src/main/java/net/sf/jsqlparser/statement/execute/ExecuteArgument.java b/src/main/java/net/sf/jsqlparser/statement/execute/ExecuteArgument.java new file mode 100644 index 0000000000..9ed550a6e4 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/execute/ExecuteArgument.java @@ -0,0 +1,62 @@ +/*- + * #%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.statement.execute; + +import java.util.Objects; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** An EXEC argument with a call-specific OUTPUT modifier. */ +public class ExecuteArgument extends ASTNodeAccessImpl implements Expression { + private Expression expression; + private boolean output; + + public ExecuteArgument(Expression expression, boolean output) { + setExpression(expression); + this.output = output; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = Objects.requireNonNull(expression, "expression"); + } + + public boolean isOutput() { + return output; + } + + public void setOutput(boolean output) { + this.output = output; + } + + @Override + public T accept(ExpressionVisitor visitor, S context) { + return visitor.visit(this, context); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + expressionPrinter.accept(expression); + if (output) { + builder.append(" OUTPUT"); + } + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/export/Export.java b/src/main/java/net/sf/jsqlparser/statement/export/Export.java index dd6628a20e..2c6934270b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/export/Export.java +++ b/src/main/java/net/sf/jsqlparser/statement/export/Export.java @@ -10,13 +10,14 @@ package net.sf.jsqlparser.statement.export; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; import net.sf.jsqlparser.statement.select.*; -public class Export implements Statement { +public class Export extends ASTNodeAccessImpl implements Statement { private Table table; private ExpressionList columns; private Select select; diff --git a/src/main/java/net/sf/jsqlparser/statement/export/ExportDataStatement.java b/src/main/java/net/sf/jsqlparser/statement/export/ExportDataStatement.java new file mode 100644 index 0000000000..1ea8e8074a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/export/ExportDataStatement.java @@ -0,0 +1,107 @@ +/*- + * #%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.statement.export; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.select.Select; + +import java.util.function.Consumer; + +/** + * BigQuery's {@code EXPORT DATA [WITH CONNECTION connection] OPTIONS (option_list) AS query}. + * + * @see Export + * statements + */ +public class ExportDataStatement extends ASTNodeAccessImpl implements Statement { + private String connectionName; + private ExpressionList options; + private Select select; + + public String getConnectionName() { + return connectionName; + } + + public void setConnectionName(String connectionName) { + this.connectionName = connectionName; + } + + public ExportDataStatement withConnectionName(String connectionName) { + setConnectionName(connectionName); + return this; + } + + public ExpressionList getOptions() { + return options; + } + + public void setOptions(ExpressionList options) { + this.options = options; + } + + public ExportDataStatement withOptions(ExpressionList options) { + setOptions(options); + return this; + } + + public Select getSelect() { + return select; + } + + public void setSelect(Select select) { + this.select = select; + } + + public ExportDataStatement withSelect(Select select) { + setSelect(select); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append, builder::append); + } + + /** Renders options and the query through the caller's expression and select visitors. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer, + Consumer
tables, Consumer expressions) { + if (privileges != null) { + if (target != null) { + target.visitTables(tables); + } + for (Privilege privilege : privileges) { + if (privilege.getColumns() != null) { + privilege.getColumns().forEach(expressions); + } + } + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeTarget.java b/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeTarget.java new file mode 100644 index 0000000000..7b815c072f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeTarget.java @@ -0,0 +1,166 @@ +/*- + * #%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.statement.grant; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.RoutineReference; + +/** + * Targets have distinct kinds; a routine signature is not a function invocation. MySQL privilege + * levels written with {@code *} are a {@link Wildcard}, and the account of MySQL + * {@code PROXY ON user} is an {@link Kind#ACCOUNT} target. + */ +public class PrivilegeTarget implements Serializable { + public enum Kind { + TABLE, SEQUENCE, DATABASE, DOMAIN, FOREIGN_DATA_WRAPPER, FOREIGN_SERVER, FUNCTION, PROCEDURE, ROUTINE, LANGUAGE, LARGE_OBJECT, PARAMETER, SCHEMA, TABLESPACE, TYPE, + /** + * The proxied account of MySQL {@code PROXY ON user}, see + * {@link PrivilegeTarget#getAccount()}. + */ + ACCOUNT + } + + /** MySQL privilege levels written with {@code *}. */ + public enum Wildcard { + /** {@code *}: the default database. */ + DEFAULT_DATABASE, + /** {@code *.*}: all databases. */ + GLOBAL, + /** + * {@code db.*}: the database named by the single entry of + * {@link PrivilegeTarget#getNames()}. + */ + DATABASE + } + + private Kind kind = Kind.TABLE; + private boolean explicitKind; + private boolean allInSchema; + private boolean defaultPrivileges; + private Wildcard wildcard; + private Grantee account; + private List> names = new ArrayList<>(); + private List routines = new ArrayList<>(); + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public boolean isExplicitKind() { + return explicitKind; + } + + public void setExplicitKind(boolean explicitKind) { + this.explicitKind = explicitKind; + } + + public boolean isAllInSchema() { + return allInSchema; + } + + public void setAllInSchema(boolean allInSchema) { + this.allInSchema = allInSchema; + } + + public boolean isDefaultPrivileges() { + return defaultPrivileges; + } + + public void setDefaultPrivileges(boolean defaultPrivileges) { + this.defaultPrivileges = defaultPrivileges; + } + + /** The MySQL wildcard privilege level, or null for named targets. */ + public Wildcard getWildcard() { + return wildcard; + } + + public void setWildcard(Wildcard wildcard) { + this.wildcard = wildcard; + } + + /** The proxied account of an {@link Kind#ACCOUNT} target. */ + public Grantee getAccount() { + return account; + } + + public void setAccount(Grantee account) { + this.account = account; + } + + public List> getNames() { + return names; + } + + public void setNames(List> names) { + this.names = names; + } + + public List getRoutines() { + return routines; + } + + public void setRoutines(List routines) { + this.routines = routines; + } + + public void visitTables(Consumer
visitor) { + if (kind == Kind.TABLE && wildcard == null && !allInSchema && !defaultPrivileges) { + for (List name : names) { + if (!name.isEmpty()) { + visitor.accept(new Table(name)); + } + } + } + } + + /** The target names as written after the optional kind, for example {@code db.*}. */ + String getTargetText() { + if (kind == Kind.ACCOUNT) { + return String.valueOf(account); + } + if (wildcard == Wildcard.DEFAULT_DATABASE) { + return "*"; + } + if (wildcard == Wildcard.GLOBAL) { + return "*.*"; + } + String targets = routines.isEmpty() + ? names.stream().map(PrivilegeTarget::joinName).collect(Collectors.joining(", ")) + : routines.stream().map(Object::toString).collect(Collectors.joining(", ")); + return wildcard == Wildcard.DATABASE ? targets + ".*" : targets; + } + + private static String joinName(List name) { + return name.stream().map(part -> part == null ? "" : part) + .collect(Collectors.joining(".")); + } + + @Override + public String toString() { + String type = kind.name().replace('_', ' '); + if (defaultPrivileges) { + return type + "S"; + } + if (allInSchema) { + return "ALL " + type + "S IN SCHEMA " + getTargetText(); + } + return (explicitKind ? type + " " : "") + getTargetText(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java b/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java new file mode 100644 index 0000000000..577ebdd52e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java @@ -0,0 +1,99 @@ +/*- + * #%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.statement.grant; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class Revoke extends ASTNodeAccessImpl implements Statement { + private PrivilegeClause clause = new PrivilegeClause(); + private boolean ifExists; + private GrantOption.Kind optionFor; + private CascadeBehavior behavior; + private boolean ignoreUnknownUser; + + public PrivilegeClause getClause() { + return clause; + } + + public void setClause(PrivilegeClause clause) { + this.clause = clause; + } + + /** MySQL {@code REVOKE IF EXISTS}. */ + public boolean isIfExists() { + return ifExists; + } + + public void setIfExists(boolean ifExists) { + this.ifExists = ifExists; + } + + /** MySQL {@code IGNORE UNKNOWN USER}. */ + public boolean isIgnoreUnknownUser() { + return ignoreUnknownUser; + } + + public void setIgnoreUnknownUser(boolean ignoreUnknownUser) { + this.ignoreUnknownUser = ignoreUnknownUser; + } + + public GrantOption.Kind getOptionFor() { + return optionFor; + } + + public void setOptionFor(GrantOption.Kind optionFor) { + this.optionFor = optionFor; + } + + public CascadeBehavior getBehavior() { + return behavior; + } + + public void setBehavior(CascadeBehavior behavior) { + this.behavior = behavior; + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + sql.append("REVOKE "); + if (ifExists) { + sql.append("IF EXISTS "); + } + if (optionFor != null) { + sql.append(optionFor).append(" OPTION FOR "); + } + clause.appendTo(sql, true, visitor); + if (clause.getGrantor() != null) { + sql.append(" GRANTED BY ").append(clause.getGrantor()); + } + if (behavior != null) { + sql.append(' ').append(behavior); + } + if (ignoreUnknownUser) { + sql.append(" IGNORE UNKNOWN USER"); + } + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/StringListView.java b/src/main/java/net/sf/jsqlparser/statement/grant/StringListView.java new file mode 100644 index 0000000000..9f220aa0eb --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/StringListView.java @@ -0,0 +1,52 @@ +/*- + * #%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.statement.grant; + +import java.util.AbstractList; +import java.util.List; +import java.util.RandomAccess; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Mutable string view over a typed list, retained for the string-based GRANT/REVOKE API. */ +final class StringListView extends AbstractList implements RandomAccess { + private final Supplier> items; + private final Function parse; + + StringListView(Supplier> items, Function parse) { + this.items = items; + this.parse = parse; + } + + @Override + public int size() { + return items.get().size(); + } + + @Override + public String get(int index) { + return String.valueOf(items.get().get(index)); + } + + @Override + public String set(int index, String value) { + return String.valueOf(items.get().set(index, parse.apply(value))); + } + + @Override + public void add(int index, String value) { + items.get().add(index, parse.apply(value)); + } + + @Override + public String remove(int index) { + return String.valueOf(items.get().remove(index)); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java b/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java index 4920aec622..7d32939f01 100644 --- a/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java +++ b/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java @@ -17,6 +17,7 @@ import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Partition; import net.sf.jsqlparser.schema.Table; @@ -32,7 +33,7 @@ import net.sf.jsqlparser.statement.update.UpdateSet; @SuppressWarnings({"PMD.CyclomaticComplexity"}) -public class Insert implements Statement { +public class Insert extends ASTNodeAccessImpl implements Statement { private Table table; private OracleHint oracleHint = null; @@ -40,7 +41,12 @@ public class Insert implements Statement { private List partitions; private Select select; private boolean onlyDefaultValues = false; - private boolean overriding = false; + + public enum OverridingMode { + NONE, SYSTEM, USER + } + + private OverridingMode overridingMode = OverridingMode.NONE; private List duplicateUpdateSets = null; private InsertModifierPriority modifierPriority = null; private boolean modifierIgnore = false; @@ -54,6 +60,7 @@ public class Insert implements Statement { private InsertConflictAction conflictAction; private InsertDuplicateAction duplicateAction; private Alias rowAlias; + private ColumnMatching columnMatching; private boolean oracleMultiInsert = false; private boolean oracleMultiInsertFirst = false; private List oracleMultiInsertBranches; @@ -226,12 +233,57 @@ public void setWithItemsList(List> withItemsList) { this.withItemsList = withItemsList; } + /** + * DuckDB matches the source columns of an INSERT either positionally or by name: + * {@code INSERT INTO tbl BY NAME SELECT ...}. + */ + public enum ColumnMatching { + BY_NAME("BY NAME"), BY_POSITION("BY POSITION"); + + private final String keywords; + + ColumnMatching(String keywords) { + this.keywords = keywords; + } + + @Override + public String toString() { + return keywords; + } + } + + public ColumnMatching getColumnMatching() { + return columnMatching; + } + + public void setColumnMatching(ColumnMatching columnMatching) { + this.columnMatching = columnMatching; + } + + public Insert withColumnMatching(ColumnMatching columnMatching) { + setColumnMatching(columnMatching); + return this; + } + + public OverridingMode getOverridingMode() { + return overridingMode; + } + + public void setOverridingMode(OverridingMode overridingMode) { + this.overridingMode = java.util.Objects.requireNonNull(overridingMode); + } + + public Insert withOverridingMode(OverridingMode overridingMode) { + setOverridingMode(overridingMode); + return this; + } + public boolean isOverriding() { - return overriding; + return overridingMode != OverridingMode.NONE; } public void setOverriding(boolean overriding) { - this.overriding = overriding; + this.overridingMode = overriding ? OverridingMode.SYSTEM : OverridingMode.NONE; } public Insert withOverriding(boolean overriding) { @@ -388,8 +440,12 @@ private void appendInsertTargetAndValues(StringBuilder sql) { sql.append(") "); } - if (overriding) { - sql.append("OVERRIDING SYSTEM VALUE "); + if (columnMatching != null) { + sql.append(columnMatching).append(" "); + } + + if (isOverriding()) { + sql.append("OVERRIDING ").append(overridingMode).append(" VALUE "); } if (partitions != null) { diff --git a/src/main/java/net/sf/jsqlparser/statement/insert/InsertBulk.java b/src/main/java/net/sf/jsqlparser/statement/insert/InsertBulk.java new file mode 100644 index 0000000000..c0fa0d4430 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/insert/InsertBulk.java @@ -0,0 +1,163 @@ +/*- + * #%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.statement.insert; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.select.OrderByElement; + +/** SQL Server's SQL declaration preceding a separate bulk-load data stream. */ +public class InsertBulk extends ASTNodeAccessImpl implements Statement { + private Table table; + private List columns = new ArrayList<>(); + private List
tablePrinter) { builder.append("REFRESH MATERIALIZED VIEW "); - if (this.refreshMode == null) { - if (concurrently) { - builder.append("CONCURRENTLY "); - } - builder.append(view); - return builder.toString(); + if (concurrently) { + builder.append("CONCURRENTLY "); } - switch (this.refreshMode) { - case WITH_DATA: - if (concurrently) { - builder.append("CONCURRENTLY "); - } - builder.append(view); - builder.append(" WITH DATA"); - break; - case WITH_NO_DATA: - builder.append(view); - if (!concurrently) { - builder.append(" WITH NO DATA"); - } - break; + tablePrinter.accept(view); + if (refreshMode == RefreshMode.WITH_DATA) { + builder.append(" WITH DATA"); + } else if (refreshMode == RefreshMode.WITH_NO_DATA) { + builder.append(" WITH NO DATA"); } - return builder.toString(); + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); } @Override diff --git a/src/main/java/net/sf/jsqlparser/statement/reindex/Reindex.java b/src/main/java/net/sf/jsqlparser/statement/reindex/Reindex.java new file mode 100644 index 0000000000..29d5a179f9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/reindex/Reindex.java @@ -0,0 +1,183 @@ +/*- + * #%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.statement.reindex; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** A PostgreSQL {@code REINDEX} statement. */ +public class Reindex extends ASTNodeAccessImpl implements Statement { + + public enum Target { + INDEX, TABLE, SCHEMA, DATABASE, SYSTEM + } + + private final List
visitor) { + Table relation = objectType == ObjectType.COLUMN && column != null ? column.getTable() + : objectType != null && objectType.isRelation() ? objectName : null; + if (relation != null) { + visitor.accept(relation); + } + } + + public StringBuilder appendTo(StringBuilder builder, Consumer
relationWriter, + Consumer columnWriter, Consumer labelWriter) { + builder.append("SECURITY LABEL "); + if (provider != null) { + builder.append("FOR ").append(provider).append(' '); + } + builder.append("ON "); + if (objectType == ObjectType.LANGUAGE && procedural) { + builder.append("PROCEDURAL "); + } + builder.append(objectType.getKeyword()).append(' '); + if (objectType == ObjectType.COLUMN) { + columnWriter.accept(column); + } else if (objectType.isRoutine()) { + builder.append(routine); + } else if (objectType == ObjectType.LARGE_OBJECT) { + builder.append(largeObjectOid); + } else if (objectType.isRelation()) { + relationWriter.accept(objectName); + } else { + builder.append(objectName); + } + builder.append(" IS "); + if (label == null) { + builder.append("NULL"); + } else { + labelWriter.accept(label); + } + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append, builder::append, builder::append).toString(); + } + + public SecurityLabel withProvider(String provider) { + setProvider(provider); + return this; + } + + public SecurityLabel withObjectName(ObjectType objectType, Table objectName) { + setObjectName(objectType, objectName); + return this; + } + + public SecurityLabel withColumn(Column column) { + setColumn(column); + return this; + } + + public SecurityLabel withRoutine(ObjectType objectType, RoutineReference routine) { + setRoutine(objectType, routine); + return this; + } + + public SecurityLabel withLargeObjectOid(long largeObjectOid) { + setLargeObjectOid(largeObjectOid); + return this; + } + + public SecurityLabel withLabel(StringValue label) { + setLabel(label); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/AllColumns.java b/src/main/java/net/sf/jsqlparser/statement/select/AllColumns.java index 08b8532e39..f667517a4b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/AllColumns.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/AllColumns.java @@ -9,92 +9,45 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.ColumnsTransformer; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; -import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; -import net.sf.jsqlparser.schema.Column; - -import java.util.ArrayList; -import java.util.List; public class AllColumns extends ASTNodeAccessImpl implements Expression { - protected ExpressionList exceptColumns; - protected List> replaceExpressions; - private String exceptKeyword; - - public AllColumns(ExpressionList exceptColumns, - List> replaceExpressions) { - this.exceptColumns = exceptColumns; - this.replaceExpressions = replaceExpressions; - this.exceptKeyword = exceptColumns != null ? "Except" : null; - } - - public AllColumns(ExpressionList exceptColumns, - List> replaceExpressions, String exceptKeyword) { - this.exceptColumns = exceptColumns; - this.replaceExpressions = replaceExpressions; - this.exceptKeyword = exceptKeyword; - } + private List transformers; public AllColumns() { - this(null, null); - } - - public ExpressionList getExceptColumns() { - return exceptColumns; + this(null); } - public AllColumns setExceptColumns(ExpressionList exceptColumns) { - this.exceptColumns = exceptColumns; - return this; + public AllColumns(List transformers) { + this.transformers = transformers; } - public ExpressionList addExceptColumn(Column column) { - if (exceptColumns == null) { - exceptColumns = new ExpressionList<>(); + public List getTransformers() { + if (transformers == null) { + transformers = new ArrayList<>(); } - exceptColumns.add(column); - return exceptColumns; + return transformers; } - public List> getReplaceExpressions() { - return replaceExpressions; - } - - public AllColumns setReplaceExpressions(List> replaceExpressions) { - this.replaceExpressions = replaceExpressions; + public AllColumns setTransformers(List transformers) { + this.transformers = transformers; return this; } - public List> addReplaceExpression(SelectItem selectItem) { - if (replaceExpressions == null) { - replaceExpressions = new ArrayList<>(); - } - replaceExpressions.add(selectItem); - return replaceExpressions; - } - - public String getExceptKeyword() { - return exceptKeyword; - } - - public AllColumns setExceptKeyword(String exceptKeyword) { - this.exceptKeyword = exceptKeyword; + public AllColumns addTransformer(ColumnsTransformer transformer) { + getTransformers().add(transformer); return this; } public StringBuilder appendTo(StringBuilder builder) { builder.append("*"); - if (exceptColumns != null && !exceptColumns.isEmpty()) { - builder.append(" ").append(exceptKeyword).append("( "); - exceptColumns.appendTo(builder); - builder.append(" )"); - } - if (replaceExpressions != null && !replaceExpressions.isEmpty()) { - builder.append(" REPLACE( "); - builder.append(Select.getStringList(replaceExpressions)); - builder.append(" )"); + for (ColumnsTransformer transformer : getTransformers()) { + builder.append(" ").append(transformer); } return builder; } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/AllTableColumns.java b/src/main/java/net/sf/jsqlparser/statement/select/AllTableColumns.java index 0030da89b2..52b55878b8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/AllTableColumns.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/AllTableColumns.java @@ -10,9 +10,8 @@ package net.sf.jsqlparser.statement.select; import java.util.List; +import net.sf.jsqlparser.expression.ColumnsTransformer; import net.sf.jsqlparser.expression.ExpressionVisitor; -import net.sf.jsqlparser.expression.operators.relational.ExpressionList; -import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.ReturningReferenceType; @@ -22,24 +21,17 @@ public class AllTableColumns extends AllColumns { private ReturningReferenceType returningReferenceType = null; private String returningQualifier = null; - public AllTableColumns(Table table, ExpressionList exceptColumns, - List> replaceExpressions, String exceptKeyword) { - super(exceptColumns, replaceExpressions, exceptKeyword); + public AllTableColumns(Table table, List transformers) { + super(transformers); this.table = table; } - public AllTableColumns(Table table, ExpressionList exceptColumns, - List> replaceExpressions) { - this(table, exceptColumns, replaceExpressions, "EXCEPT"); - } - public AllTableColumns(Table table) { - this(table, null, null); + this(table, (List) null); } public AllTableColumns(Table table, AllColumns allColumns) { - this(table, allColumns.exceptColumns, allColumns.replaceExpressions, - allColumns.getExceptKeyword()); + this(table, allColumns.getTransformers()); } public Table getTable() { diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java index 6b10480319..b1299dfd12 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java @@ -9,13 +9,12 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.Collection; +import java.util.List; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.piped.FromQuery; -import java.util.Collection; -import java.util.List; - public interface FromItemVisitor { default T visitFromItem(FromItem fromItem, S context) { @@ -43,6 +42,18 @@ default T visitJoins(Collection joins, S context) { return null; } + /** + * Existing visitors continue through the input relation; adapters also visit clause + * expressions. + */ + default T visit(MatchRecognize matchRecognize, S context) { + return matchRecognize.getInput().accept(this, context); + } + + default void visit(MatchRecognize matchRecognize) { + visit(matchRecognize, null); + } + T visit(Table tableName, S context); default void visit(Table tableName) { @@ -85,6 +96,22 @@ default void visit(PlainSelect plainSelect) { this.visit(plainSelect, null); } + default T visit(PivotQuery pivotQuery, S context) { + return null; + } + + default void visit(PivotQuery pivotQuery) { + this.visit(pivotQuery, null); + } + + default T visit(UnPivotQuery unPivotQuery, S context) { + return null; + } + + default void visit(UnPivotQuery unPivotQuery) { + this.visit(unPivotQuery, null); + } + T visit(SetOperationList setOperationList, S context); default void visit(SetOperationList setOperationList) { diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java index 23bd480b82..157582acb0 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java @@ -9,17 +9,17 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.ArrayList; +import java.util.Collection; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.piped.FromQuery; -import java.util.ArrayList; -import java.util.Collection; - @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) public class FromItemVisitorAdapter implements FromItemVisitor { private SelectVisitor selectVisitor; @@ -86,6 +86,21 @@ public T visitJoins(Collection joins, S context) { return null; } + @Override + public T visit(MatchRecognize matchRecognize, S context) { + matchRecognize.getInput().accept(this, context); + matchRecognize + .forEachExpression(expression -> expression.accept(expressionVisitor, context)); + PivotVisitorAdapter pivots = new PivotVisitorAdapter<>(expressionVisitor); + if (matchRecognize.getPivot() != null) { + matchRecognize.getPivot().accept(pivots, context); + } + if (matchRecognize.getUnPivot() != null) { + matchRecognize.getUnPivot().accept(pivots, context); + } + return null; + } + @Override public T visit(Table table, S context) { return null; @@ -93,23 +108,29 @@ public T visit(Table table, S context) { @Override public T visit(ParenthesedSelect select, S context) { - return select.getPlainSelect().accept(selectVisitor, context); + return select.getSelect().accept(selectVisitor, context); } @Override public T visit(LateralSubSelect lateralSubSelect, S context) { - return lateralSubSelect.getPlainSelect().accept(selectVisitor, context); + return lateralSubSelect.getSelect().accept(selectVisitor, context); } @Override public T visit(TableFunction tableFunction, S context) { - + if (tableFunction.getFunctions() != null) { + for (Function function : tableFunction.getFunctions()) { + function.accept(expressionVisitor, context); + } + } return null; } @Override public T visit(ParenthesedFromItem fromItem, S context) { - return fromItem.getFromItem().accept(this, context); + T result = fromItem.getFromItem().accept(this, context); + visitJoins(fromItem.getJoins(), context); + return result; } @Override @@ -125,6 +146,16 @@ public T visit(PlainSelect plainSelect, S context) { return plainSelect.accept(selectVisitor, context); } + @Override + public T visit(PivotQuery pivotQuery, S context) { + return pivotQuery.accept(selectVisitor, context); + } + + @Override + public T visit(UnPivotQuery unPivotQuery, S context) { + return unPivotQuery.accept(selectVisitor, context); + } + @Override public T visit(SetOperationList setOperationList, S context) { ArrayList results = new ArrayList<>(); diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FunctionAllColumns.java b/src/main/java/net/sf/jsqlparser/statement/select/FunctionAllColumns.java index 7a4fa5ce2d..97d54e4500 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FunctionAllColumns.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FunctionAllColumns.java @@ -17,7 +17,7 @@ public class FunctionAllColumns extends AllColumns { private Function function; public FunctionAllColumns(Function function) { - super(null, null, null); + super(null); this.function = function; } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/GroupByElement.java b/src/main/java/net/sf/jsqlparser/statement/select/GroupByElement.java index 63b6b479d9..f7be412e6d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/GroupByElement.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/GroupByElement.java @@ -16,16 +16,25 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Objects; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; public class GroupByElement implements Serializable { + public enum SortDirection { + ASC, DESC + } + + private final List groupBySortDirections = new ArrayList<>(); private ExpressionList groupByExpressions = new ExpressionList<>(); private List> groupingSets = new ArrayList<>(); // postgres rollup is an ExpressionList private boolean mysqlWithRollup = false; + private boolean all = false; + private boolean distinct = false; public boolean isUsingBrackets() { return groupByExpressions.isUsingBrackets(); @@ -45,6 +54,9 @@ public ExpressionList getGroupByExpressions() { } public void setGroupByExpressions(ExpressionList groupByExpressions) { + if (this.groupByExpressions != groupByExpressions) { + groupBySortDirections.clear(); + } this.groupByExpressions = groupByExpressions; } @@ -68,35 +80,89 @@ public void addGroupingSet(ExpressionList list) { this.groupingSets.add(list); } + /** Returns the explicit direction at a grouping-list position, or null if omitted. */ + public SortDirection getGroupBySortDirection(int index) { + Objects.checkIndex(index, groupByExpressions.size()); + return index < groupBySortDirections.size() ? groupBySortDirections.get(index) : null; + } + + /** Directions belong to list positions. Replacing the expression list clears them. */ + public void setGroupBySortDirection(int index, SortDirection direction) { + Objects.checkIndex(index, groupByExpressions.size()); + while (groupBySortDirections.size() <= index) { + groupBySortDirections.add(null); + } + groupBySortDirections.set(index, direction); + } + + public boolean hasGroupBySortDirections() { + if (groupByExpressions != null && !groupBySortDirections.isEmpty()) { + for (int i = 0; i < groupByExpressions.size(); i++) { + if (getGroupBySortDirection(i) != null) { + return true; + } + } + } + return false; + } + @Override - @SuppressWarnings({"PMD.CyclomaticComplexity"}) public String toString() { - StringBuilder b = new StringBuilder(); - b.append("GROUP BY "); + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append, builder::append); + return builder.toString(); + } + /** Shares clause layout while letting a deparser visit each expression. */ + public void appendTo(StringBuilder builder, Consumer> listRenderer, + Consumer expressionRenderer) { + builder.append("GROUP BY "); + if (all) { + builder.append("ALL "); + } + if (distinct) { + builder.append("DISTINCT "); + } if (groupByExpressions != null) { - b.append(groupByExpressions); + if (hasGroupBySortDirections()) { + appendOrderedExpressions(builder, expressionRenderer); + } else { + listRenderer.accept(groupByExpressions); + } } - - int i = 0; if (!groupingSets.isEmpty()) { - if (b.charAt(b.length() - 1) != ' ') { - b.append(' '); + if (builder.charAt(builder.length() - 1) != ' ') { + builder.append(' '); } - b.append("GROUPING SETS ("); - for (ExpressionList expressionList : groupingSets) { - b.append(i++ > 0 ? ", " : "").append(Select.getStringList( - expressionList, - true, expressionList instanceof ParenthesedExpressionList)); + builder.append("GROUPING SETS ("); + for (int i = 0; i < groupingSets.size(); i++) { + builder.append(i > 0 ? ", " : ""); + listRenderer.accept(groupingSets.get(i)); } - b.append(")"); + builder.append(")"); } - if (isMysqlWithRollup()) { - b.append(" WITH ROLLUP"); + builder.append(" WITH ROLLUP"); } + } - return b.toString(); + private void appendOrderedExpressions(StringBuilder builder, + Consumer expressionRenderer) { + boolean brackets = groupByExpressions instanceof ParenthesedExpressionList; + if (brackets) { + builder.append('('); + } + for (int i = 0; i < groupByExpressions.size(); i++) { + builder.append(i > 0 ? ", " : ""); + expressionRenderer.accept(groupByExpressions.get(i)); + SortDirection direction = getGroupBySortDirection(i); + if (direction != null) { + builder.append(' ').append(direction); + } + } + if (brackets) { + builder.append(')'); + } } public GroupByElement withGroupByExpressions(ExpressionList groupByExpressions) { @@ -115,9 +181,9 @@ public GroupByElement addGroupByExpressions(Expression... groupByExpressions) { public GroupByElement addGroupByExpressions( Collection groupByExpressions) { - ExpressionList collection = - Optional.ofNullable(getGroupByExpressions()).orElseGet(ExpressionList::new); - Collections.addAll(collection, groupByExpressions); + ExpressionList collection = + Optional.ofNullable(getGroupByExpressionList()).orElseGet(ExpressionList::new); + collection.addAll(groupByExpressions); return this.withGroupByExpressions(collection); } @@ -142,4 +208,22 @@ public GroupByElement setMysqlWithRollup(boolean mysqlWithRollup) { this.mysqlWithRollup = mysqlWithRollup; return this; } + + public boolean isAll() { + return all; + } + + public GroupByElement setAll(boolean all) { + this.all = all; + return this; + } + + public boolean isDistinct() { + return distinct; + } + + public GroupByElement setDistinct(boolean distinct) { + this.distinct = distinct; + return this; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java b/src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java new file mode 100644 index 0000000000..a571c4cf5c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java @@ -0,0 +1,73 @@ +/*- + * #%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.statement.select; + +import java.io.Serializable; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; + +/** + * Models one column entry of the ClickHouse {@code INTERPOLATE} clause, which computes the values + * of non-sorted columns for the rows generated by {@code ORDER BY ... WITH FILL} (see + * ClickHouse: + * ORDER BY). + *

+ * {@code col AS expr} evaluates {@code expr} for the generated rows, while a bare {@code col} (a + * {@code null} expression) repeats the previous value of the column. + */ +public class InterpolateElement implements Serializable { + + private Column column; + private Expression expression; + + public InterpolateElement() { + super(); + } + + public InterpolateElement(Column column, Expression expression) { + this.column = column; + this.expression = expression; + } + + public Column getColumn() { + return column; + } + + public void setColumn(Column column) { + this.column = column; + } + + public InterpolateElement withColumn(Column column) { + setColumn(column); + return this; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public InterpolateElement withExpression(Expression expression) { + setExpression(expression); + return this; + } + + @Override + public String toString() { + if (expression != null) { + return column + " AS " + expression; + } + return column.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Join.java b/src/main/java/net/sf/jsqlparser/statement/select/Join.java index 191465e27f..d06a5df1f8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Join.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Join.java @@ -25,6 +25,7 @@ public class Join extends ASTNodeAccessImpl { private final LinkedList onExpressions = new LinkedList<>(); private final LinkedList usingColumns = new LinkedList<>(); + private boolean asOf = false; private boolean outer = false; private boolean right = false; private boolean left = false; @@ -35,16 +36,34 @@ public class Join extends ASTNodeAccessImpl { private boolean simple = false; private boolean cross = false; private boolean semi = false; + private boolean anti = false; + private Long approxNearest; + private Expression similarity; private boolean any = false; private boolean all = false; private boolean straight = false; private boolean apply = false; private boolean fetch = false; + private boolean array = false; private FromItem fromItem; private KSQLJoinWindow joinWindow; private JoinHint joinHint = null; + /** Whether the join uses nearest-match ASOF semantics. */ + public boolean isAsOf() { + return asOf; + } + + public void setAsOf(boolean asOf) { + this.asOf = asOf; + } + + public Join withAsOf(boolean asOf) { + setAsOf(asOf); + return this; + } + public boolean isSimple() { return simple; } @@ -73,7 +92,10 @@ public boolean isInnerJoin() { || cross /* Natural Join */ - || natural); + || natural + + /* Array Join */ + || array); } /** @@ -175,6 +197,49 @@ public Join withFetch(boolean b) { * * @return true if is a "SEMI" join */ + /** + * DuckDB 2.0's similarity join: + * {@code JOIN products APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(...)}. + */ + public Long getApproxNearest() { + return approxNearest; + } + + public void setApproxNearest(Long approxNearest) { + this.approxNearest = approxNearest; + } + + public Join withApproxNearest(Long approxNearest) { + setApproxNearest(approxNearest); + return this; + } + + public Expression getSimilarity() { + return similarity; + } + + public void setSimilarity(Expression similarity) { + this.similarity = similarity; + } + + public Join withSimilarity(Expression similarity) { + setSimilarity(similarity); + return this; + } + + public boolean isAnti() { + return anti; + } + + public void setAnti(boolean b) { + anti = b; + } + + public Join withAnti(boolean b) { + this.setAnti(b); + return this; + } + public boolean isSemi() { return semi; } @@ -337,6 +402,19 @@ public Join withCross(boolean cross) { return this; } + public boolean isArray() { + return array; + } + + public void setArray(boolean array) { + this.array = array; + } + + public Join withArray(boolean array) { + this.setArray(array); + return this; + } + /** * Returns the right item of the join */ @@ -447,6 +525,70 @@ public Join setJoinHint(JoinHint joinHint) { return this; } + /** Appends the join modifiers shared by model rendering and expression deparsers. */ + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendJoinTypeTo(StringBuilder builder) { + if (isAsOf()) { + builder.append("ASOF "); + } + if (isNatural()) { + builder.append("NATURAL "); + } + + if (isAny()) { + builder.append("ANY "); + } else if (isAll()) { + builder.append("ALL "); + } + + if (isRight()) { + builder.append("RIGHT "); + } else if (isFull()) { + builder.append("FULL "); + } else if (isLeft()) { + builder.append("LEFT "); + } else if (isCross()) { + builder.append("CROSS "); + } + + if (isOuter()) { + builder.append("OUTER "); + } else if (isInner()) { + builder.append("INNER "); + } else if (isSemi()) { + builder.append("SEMI "); + } else if (isAnti()) { + builder.append("ANTI "); + } + + if (isArray()) { + builder.append("ARRAY "); + } + + return builder; + } + + /** Appends the join keyword, hint and FETCH modifier, followed by a space. */ + public StringBuilder appendJoinKeywordTo(StringBuilder builder) { + if (isStraight()) { + builder.append("STRAIGHT_JOIN "); + } else if (isApply()) { + builder.append("APPLY "); + } else { + if (joinHint != null && joinHint.getPosition() == JoinHint.Position.BEFORE_JOIN) { + builder.append(joinHint).append(' '); + } + builder.append("JOIN "); + if (joinHint != null && joinHint.getPosition() == JoinHint.Position.AFTER_JOIN) { + builder.append(joinHint).append(' '); + } + if (fetch) { + builder.append("FETCH "); + } + } + return builder; + } + @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public String toString() { @@ -461,51 +603,17 @@ public String toString() { } else if (isSimple()) { builder.append(fromItem); } else { - if (isNatural()) { - builder.append("NATURAL "); - } - - if (isAny()) { - builder.append("ANY "); - } else if (isAll()) { - builder.append("ALL "); - } - - if (isRight()) { - builder.append("RIGHT "); - } else if (isFull()) { - builder.append("FULL "); - } else if (isLeft()) { - builder.append("LEFT "); - } else if (isCross()) { - builder.append("CROSS "); - } - - if (isOuter()) { - builder.append("OUTER "); - } else if (isInner()) { - builder.append("INNER "); - } else if (isSemi()) { - builder.append("SEMI "); - } - - if (isStraight()) { - builder.append("STRAIGHT_JOIN "); - } else if (isApply()) { - builder.append("APPLY "); - } else { - if (joinHint != null) { - builder.append(joinHint).append(" "); - } - builder.append("JOIN "); - if (fetch) { - builder.append("FETCH "); - } - } + appendJoinTypeTo(builder); + appendJoinKeywordTo(builder); builder.append(fromItem).append((joinWindow != null) ? " WITHIN " + joinWindow : ""); } + if (approxNearest != null) { + builder.append(" APPROX NEAREST ").append(approxNearest) + .append(" BY SIMILARITY ").append(similarity); + } + for (Expression onExpression : onExpressions) { builder.append(" ON ").append(onExpression); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/JoinHint.java b/src/main/java/net/sf/jsqlparser/statement/select/JoinHint.java index 099f73042f..d236bc4e9f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/JoinHint.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/JoinHint.java @@ -9,23 +9,44 @@ */ package net.sf.jsqlparser.statement.select; +import java.io.Serializable; +import java.util.Objects; + /** - * Hints (Transact-SQL) - Join + * SQL Server join hints precede JOIN; Doris distribution hints follow it in square brackets. * * @link Hints * (Transact-SQL) - Join */ -public class JoinHint { +public class JoinHint implements Serializable { + public enum Position { + BEFORE_JOIN, AFTER_JOIN + } + private final String keyword; + private final Position position; public JoinHint(String keyword) { + this(keyword, Position.BEFORE_JOIN); + } + + public JoinHint(String keyword, Position position) { this.keyword = keyword; + this.position = Objects.requireNonNull(position, "position"); + } + + public String getKeyword() { + return keyword; + } + + public Position getPosition() { + return position; } @Override public String toString() { - return keyword; + return position == Position.AFTER_JOIN ? "[" + keyword + "]" : keyword; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/KSQLJoinWindow.java b/src/main/java/net/sf/jsqlparser/statement/select/KSQLJoinWindow.java index 5afbecd3de..16daac002a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/KSQLJoinWindow.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/KSQLJoinWindow.java @@ -9,7 +9,6 @@ */ package net.sf.jsqlparser.statement.select; -import java.util.Locale; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import static net.sf.jsqlparser.statement.select.KSQLWindow.TimeUnit; @@ -23,9 +22,37 @@ public class KSQLJoinWindow extends ASTNodeAccessImpl { private TimeUnit beforeTimeUnit; private long afterDuration; private TimeUnit afterTimeUnit; + private boolean usingBrackets = true; + private KSQLWindow.Duration gracePeriod; + + public boolean isUsingBrackets() { + return usingBrackets || beforeAfter; + } + + public void setUsingBrackets(boolean usingBrackets) { + this.usingBrackets = usingBrackets; + } + + public KSQLJoinWindow withUsingBrackets(boolean usingBrackets) { + setUsingBrackets(usingBrackets); + return this; + } + + public KSQLWindow.Duration getGracePeriod() { + return gracePeriod; + } + + public void setGracePeriod(KSQLWindow.Duration gracePeriod) { + this.gracePeriod = gracePeriod; + } + + public KSQLJoinWindow withGracePeriod(KSQLWindow.Duration gracePeriod) { + setGracePeriod(gracePeriod); + return this; + } public final static TimeUnit from(String timeUnitStr) { - return Enum.valueOf(TimeUnit.class, timeUnitStr.toUpperCase(Locale.ROOT)); + return TimeUnit.from(timeUnitStr); } public boolean isBeforeAfterWindow() { @@ -86,11 +113,23 @@ public void setAfterTimeUnit(TimeUnit afterTimeUnit) { @Override public String toString() { + StringBuilder builder = new StringBuilder(); + if (isUsingBrackets()) { + builder.append('('); + } if (isBeforeAfterWindow()) { - return "(" + beforeDuration + " " + beforeTimeUnit + ", " + afterDuration + " " - + afterTimeUnit + ")"; + builder.append(beforeDuration).append(' ').append(beforeTimeUnit) + .append(", ").append(afterDuration).append(' ').append(afterTimeUnit); + } else { + builder.append(duration).append(' ').append(timeUnit); + } + if (isUsingBrackets()) { + builder.append(')'); + } + if (gracePeriod != null) { + builder.append(" GRACE PERIOD ").append(gracePeriod); } - return "(" + duration + " " + timeUnit + ")"; + return builder.toString(); } public KSQLJoinWindow withDuration(long duration) { diff --git a/src/main/java/net/sf/jsqlparser/statement/select/KSQLWindow.java b/src/main/java/net/sf/jsqlparser/statement/select/KSQLWindow.java index ea50d4cb6a..eae98921f1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/KSQLWindow.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/KSQLWindow.java @@ -21,6 +21,47 @@ public class KSQLWindow extends ASTNodeAccessImpl { private TimeUnit sizeTimeUnit; private long advanceDuration; private TimeUnit advanceTimeUnit; + private Duration gracePeriod; + + /** A non-negative duration with its SQL time unit, including an explicit zero. */ + public static final class Duration implements java.io.Serializable { + private final long value; + private final TimeUnit timeUnit; + + public Duration(long value, TimeUnit timeUnit) { + if (value < 0) { + throw new IllegalArgumentException("Duration must not be negative"); + } + this.value = value; + this.timeUnit = java.util.Objects.requireNonNull(timeUnit, "timeUnit"); + } + + public long getValue() { + return value; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + @Override + public String toString() { + return value + " " + timeUnit; + } + } + + public Duration getGracePeriod() { + return gracePeriod; + } + + public void setGracePeriod(Duration gracePeriod) { + this.gracePeriod = gracePeriod; + } + + public KSQLWindow withGracePeriod(Duration gracePeriod) { + setGracePeriod(gracePeriod); + return this; + } public KSQLWindow() {} @@ -82,14 +123,20 @@ public void setAdvanceTimeUnit(TimeUnit advanceTimeUnit) { @Override public String toString() { + StringBuilder builder = new StringBuilder(); if (isHoppingWindow()) { - return "HOPPING (" + "SIZE " + sizeDuration + " " + sizeTimeUnit + ", " + - "ADVANCE BY " + advanceDuration + " " + advanceTimeUnit + ")"; + builder.append("HOPPING (SIZE ").append(sizeDuration).append(' ').append(sizeTimeUnit) + .append(", ADVANCE BY ").append(advanceDuration).append(' ') + .append(advanceTimeUnit); } else if (isSessionWindow()) { - return "SESSION (" + sizeDuration + " " + sizeTimeUnit + ")"; + builder.append("SESSION (").append(sizeDuration).append(' ').append(sizeTimeUnit); } else { - return "TUMBLING (" + "SIZE " + sizeDuration + " " + sizeTimeUnit + ")"; + builder.append("TUMBLING (SIZE ").append(sizeDuration).append(' ').append(sizeTimeUnit); + } + if (gracePeriod != null) { + builder.append(", GRACE PERIOD ").append(gracePeriod); } + return builder.append(')').toString(); } public KSQLWindow withSizeDuration(long sizeDuration) { diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Limit.java b/src/main/java/net/sf/jsqlparser/statement/select/Limit.java index ec5923a3f6..87d9729851 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Limit.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Limit.java @@ -32,6 +32,8 @@ public class Limit extends ASTNodeAccessImpl { */ private ExpressionList byExpressions; + private String percentUnit; + public Expression getOffset() { return offset; } @@ -72,19 +74,39 @@ public void setLimitNull(boolean b) { } } + /** + * DuckDB's {@code %} or {@code PERCENT} after the row count, which then is a percentage of the + * rows, or {@code null}. + */ + public String getPercentUnit() { + return percentUnit; + } + + public Limit setPercentUnit(String percentUnit) { + this.percentUnit = percentUnit; + return this; + } + + String percentUnitToString() { + if (percentUnit == null) { + return ""; + } + return "%".equals(percentUnit) ? "%" : " " + percentUnit; + } + @Override public String toString() { String retVal = " LIMIT "; if (rowCount instanceof AllValue || rowCount instanceof NullValue) { // no offset allowed - retVal += rowCount; + retVal += rowCount + percentUnitToString(); } else { if (null != offset) { retVal += offset + ", "; } if (null != rowCount) { - retVal += rowCount; + retVal += rowCount + percentUnitToString(); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/MatchRecognize.java b/src/main/java/net/sf/jsqlparser/statement/select/MatchRecognize.java new file mode 100644 index 0000000000..1744277a82 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/MatchRecognize.java @@ -0,0 +1,418 @@ +/*- + * #%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.statement.select; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.HashSet; +import java.util.Locale; +import net.sf.jsqlparser.schema.MultiPartName; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** A relation transformed by MATCH_RECOGNIZE. Input and result aliases are independent. */ +public class MatchRecognize extends AbstractFromitem { + public enum SkipMode { + PAST_LAST_ROW, TO_NEXT_ROW, TO_FIRST, TO_LAST, TO_VARIABLE + } + + public enum RowsPerMatch { + ONE, ALL + } + + public enum EmptyMatchMode { + SHOW, OMIT, WITH_UNMATCHED + } + + private RowsPerMatch rowsPerMatch; + private EmptyMatchMode emptyMatchMode; + private String skipVariable; + private List subsets = new ArrayList<>(); + private FromItem input; + private ExpressionList partitionBy = new ExpressionList<>(); + private List orderByElements = new ArrayList<>(); + private List> measures = new ArrayList<>(); + private SkipMode skipMode; + private RowPattern pattern; + private List definitions = new ArrayList<>(); + private Options options; + + public MatchRecognize(FromItem input) { + this.input = input; + } + + public FromItem getInput() { + return input; + } + + public MatchRecognize setInput(FromItem input) { + this.input = input; + return this; + } + + public ExpressionList getPartitionBy() { + return partitionBy; + } + + public MatchRecognize setPartitionBy(ExpressionList partitionBy) { + this.partitionBy = partitionBy; + return this; + } + + public List getOrderByElements() { + return orderByElements; + } + + public MatchRecognize setOrderByElements(List elements) { + orderByElements = elements; + return this; + } + + public List> getMeasures() { + return measures; + } + + public MatchRecognize setMeasures(List> measures) { + this.measures = measures; + return this; + } + + public SkipMode getSkipMode() { + return skipMode; + } + + public MatchRecognize setSkipMode(SkipMode mode) { + skipMode = mode; + return this; + } + + public RowsPerMatch getRowsPerMatch() { + return rowsPerMatch; + } + + public MatchRecognize setRowsPerMatch(RowsPerMatch value) { + rowsPerMatch = value; + return this; + } + + public EmptyMatchMode getEmptyMatchMode() { + return emptyMatchMode; + } + + public MatchRecognize setEmptyMatchMode(EmptyMatchMode value) { + emptyMatchMode = value; + return this; + } + + public String getSkipVariable() { + return skipVariable; + } + + public MatchRecognize setSkipVariable(String value) { + skipVariable = value; + return this; + } + + public List getSubsets() { + return subsets; + } + + public MatchRecognize setSubsets(List value) { + subsets = value; + return this; + } + + public RowPattern getPattern() { + return pattern; + } + + public MatchRecognize setPattern(RowPattern pattern) { + this.pattern = pattern; + return this; + } + + public List getDefinitions() { + return definitions; + } + + public MatchRecognize setDefinitions(List definitions) { + this.definitions = definitions; + return this; + } + + public Options getOptions() { + return options; + } + + public MatchRecognize setOptions(Options options) { + this.options = options; + return this; + } + + @Override + public T accept(FromItemVisitor visitor, S context) { + return visitor.visit(this, context); + } + + /** Case-insensitive pattern names for syntactic table discovery, including SUBSET names. */ + public Set getPatternVariableNames() { + Set names = new HashSet<>(); + if (pattern != null) { + pattern.accept(new RowPatternVisitorAdapter() { + @Override + public Void visit(RowPattern.Variable variable, S context) { + names.add(normalizeVariableName(variable.getName())); + return null; + } + }, null); + } + subsets.forEach(subset -> names.add(normalizeVariableName(subset.getName()))); + return names; + } + + public static String normalizeVariableName(String name) { + return name == null ? null : MultiPartName.unquote(name).toLowerCase(Locale.ROOT); + } + + /** Visits expression roots once; normal expression visitors traverse their descendants. */ + public void forEachExpression(Consumer expressions) { + partitionBy.forEach(expressions); + orderByElements.forEach(order -> expressions.accept(order.getExpression())); + forEachPatternExpression(expressions); + } + + /** Expression roots where pattern variables, rather than input aliases, are in scope. */ + public void forEachPatternExpression(Consumer expressions) { + measures.forEach(measure -> expressions.accept(measure.getExpression())); + pattern.forEachExpression(expressions); + definitions.forEach(definition -> expressions.accept(definition.getExpression())); + } + + /** Shared clause rendering, retaining the caller's source and expression customization. */ + public StringBuilder appendTo(StringBuilder builder, Consumer sources, + Consumer expressions, Consumer ordering, + Consumer pivots, Consumer unpivots) { + sources.accept(input); + builder.append(" MATCH_RECOGNIZE ("); + appendInputExpressions(builder, expressions, ordering); + appendRowOutput(builder); + appendSkip(builder); + builder.append("PATTERN ("); + pattern.appendTo(builder, expressions); + builder.append(')'); + appendSubsets(builder); + builder.append(" DEFINE "); + appendList(builder, definitions, definition -> { + builder.append(definition.getName()).append(" AS "); + expressions.accept(definition.getExpression()); + }); + appendOptions(builder); + builder.append(')'); + appendResultModifiers(builder, pivots, unpivots); + return builder; + } + + private void appendInputExpressions(StringBuilder builder, Consumer expressions, + Consumer ordering) { + if (!partitionBy.isEmpty()) { + builder.append("PARTITION BY "); + appendList(builder, partitionBy, expressions); + builder.append(' '); + } + if (!orderByElements.isEmpty()) { + builder.append("ORDER BY "); + appendList(builder, orderByElements, ordering); + builder.append(' '); + } + if (!measures.isEmpty()) { + builder.append("MEASURES "); + appendList(builder, measures, item -> { + expressions.accept(item.getExpression()); + if (item.getAlias() != null) { + builder.append(item.getAlias()); + } + }); + builder.append(' '); + } + } + + private void appendRowOutput(StringBuilder builder) { + if (rowsPerMatch != null) { + builder.append(rowsPerMatch == RowsPerMatch.ONE ? "ONE ROW PER MATCH " + : "ALL ROWS PER MATCH "); + } + if (emptyMatchMode != null) { + switch (emptyMatchMode) { + case SHOW: + builder.append("SHOW EMPTY MATCHES "); + break; + case OMIT: + builder.append("OMIT EMPTY MATCHES "); + break; + default: + builder.append("WITH UNMATCHED ROWS "); + } + } + } + + private void appendSkip(StringBuilder builder) { + if (skipMode != null) { + builder.append("AFTER MATCH SKIP "); + switch (skipMode) { + case PAST_LAST_ROW: + builder.append("PAST LAST ROW"); + break; + case TO_NEXT_ROW: + builder.append("TO NEXT ROW"); + break; + case TO_FIRST: + builder.append("TO FIRST ").append(skipVariable); + break; + case TO_LAST: + builder.append("TO LAST ").append(skipVariable); + break; + default: + builder.append("TO ").append(skipVariable); + } + builder.append(' '); + } + } + + private void appendSubsets(StringBuilder builder) { + if (!subsets.isEmpty()) { + builder.append(" SUBSET "); + appendList(builder, subsets, subset -> { + builder.append(subset.getName()).append(" = ("); + appendList(builder, subset.getVariables(), name -> builder.append(name)); + builder.append(')'); + }); + } + } + + private void appendOptions(StringBuilder builder) { + if (options != null) { + builder.append(" OPTIONS ("); + if (options.getUseLongestMatch() != null) { + builder.append("use_longest_match = ") + .append(options.getUseLongestMatch() ? "TRUE" : "FALSE"); + } + builder.append(')'); + } + } + + private void appendResultModifiers(StringBuilder builder, Consumer pivots, + Consumer unpivots) { + if (getPivot() != null) { + pivots.accept(getPivot()); + } + if (getUnPivot() != null) { + unpivots.accept(getUnPivot()); + } + if (getAlias() != null) { + builder.append(getAlias()); + } + if (getSampleClause() != null) { + builder.append(getSampleClause()); + } + } + + private static void appendList(StringBuilder builder, List elements, + Consumer append) { + for (int i = 0; i < elements.size(); i++) { + if (i > 0) { + builder.append(", "); + } + append.accept(elements.get(i)); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, source -> builder.append(source), + expression -> builder.append(expression), order -> builder.append(order), + pivot -> builder.append(" ").append(pivot), + unpivot -> builder.append(" ").append(unpivot)) + .toString(); + } + + public static class Subset extends ASTNodeAccessImpl { + private String name; + private List variables; + + public Subset(String name, List variables) { + this.name = name; + this.variables = variables; + } + + public String getName() { + return name; + } + + public Subset setName(String name) { + this.name = name; + return this; + } + + public List getVariables() { + return variables; + } + + public Subset setVariables(List variables) { + this.variables = variables; + return this; + } + } + + public static class Definition extends ASTNodeAccessImpl { + private String name; + private Expression expression; + + public Definition(String name, Expression expression) { + this.name = name; + this.expression = expression; + } + + public String getName() { + return name; + } + + public Definition setName(String name) { + this.name = name; + return this; + } + + public Expression getExpression() { + return expression; + } + + public Definition setExpression(Expression expression) { + this.expression = expression; + return this; + } + } + + /** The presence of this object distinguishes an empty OPTIONS clause from an omitted one. */ + public static class Options extends ASTNodeAccessImpl { + private Boolean useLongestMatch; + + public Boolean getUseLongestMatch() { + return useLongestMatch; + } + + public Options setUseLongestMatch(Boolean value) { + useLongestMatch = value; + return this; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java b/src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java new file mode 100644 index 0000000000..b70f2fdc4b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java @@ -0,0 +1,75 @@ +/*- + * #%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.statement.select; + +import java.io.Serializable; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * Models the legacy MySQL {@code PROCEDURE ANALYSE()} clause. + * + * @see MySQL 5.7 Reference + * Manual + */ +public class MySqlProcedureAnalyse extends ASTNodeAccessImpl implements Serializable { + + private LongValue maxElements; + private LongValue maxMemory; + + public MySqlProcedureAnalyse() {} + + public MySqlProcedureAnalyse(LongValue maxElements, LongValue maxMemory) { + this.maxElements = maxElements; + this.maxMemory = maxMemory; + } + + public LongValue getMaxElements() { + return maxElements; + } + + public void setMaxElements(LongValue maxElements) { + this.maxElements = maxElements; + } + + public MySqlProcedureAnalyse withMaxElements(LongValue maxElements) { + setMaxElements(maxElements); + return this; + } + + public LongValue getMaxMemory() { + return maxMemory; + } + + public void setMaxMemory(LongValue maxMemory) { + this.maxMemory = maxMemory; + } + + public MySqlProcedureAnalyse withMaxMemory(LongValue maxMemory) { + setMaxMemory(maxMemory); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(" PROCEDURE ANALYSE("); + if (maxElements != null) { + builder.append(maxElements); + if (maxMemory != null) { + builder.append(", ").append(maxMemory); + } + } + return builder.append(")"); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoClause.java b/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoClause.java index 5c5f93babd..16a0755fc0 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoClause.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoClause.java @@ -10,7 +10,11 @@ package net.sf.jsqlparser.statement.select; import java.io.Serializable; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.UserVariable; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; public class MySqlSelectIntoClause extends ASTNodeAccessImpl implements Serializable { @@ -20,7 +24,7 @@ public enum Position { } public enum Type { - OUTFILE, DUMPFILE + OUTFILE, DUMPFILE, VARIABLES } public enum FieldsKeyword { @@ -30,6 +34,7 @@ public enum FieldsKeyword { private Position position = Position.TRAILING; private Type type; private StringValue fileName; + private ExpressionList variables; private String characterSet; private FieldsKeyword fieldsKeyword; private StringValue fieldsTerminatedBy; @@ -60,6 +65,14 @@ public void setType(Type type) { this.type = type; } + public ExpressionList getVariables() { + return variables; + } + + public void setVariables(ExpressionList variables) { + this.variables = variables; + } + public StringValue getFileName() { return fileName; } @@ -142,7 +155,19 @@ public boolean hasLinesClause() { } public StringBuilder appendTo(StringBuilder builder) { - builder.append("INTO ").append(type); + return appendTo(builder, expression -> builder.append(expression)); + } + + /** Shares INTO rendering while allowing deparsers to visit variable targets. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) { + builder.append("INTO "); + if (type == Type.VARIABLES) { + if (variables != null) { + expressionRenderer.accept(variables); + } + return builder; + } + builder.append(type); appendFileName(builder); appendCharacterSet(builder); appendFieldsClause(builder); diff --git a/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectModifier.java b/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectModifier.java new file mode 100644 index 0000000000..60262e16e0 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectModifier.java @@ -0,0 +1,18 @@ +/*- + * #%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.statement.select; + +/** + * MySQL SELECT modifiers, in rendering order. SQL_CACHE is retained for older MySQL releases and + * MariaDB. DISTINCT and ALL remain part of the common SELECT grammar. + */ +public enum MySqlSelectModifier { + HIGH_PRIORITY, STRAIGHT_JOIN, SQL_SMALL_RESULT, SQL_BIG_RESULT, SQL_BUFFER_RESULT, SQL_CACHE, SQL_NO_CACHE, SQL_CALC_FOUND_ROWS +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java new file mode 100644 index 0000000000..185166798d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java @@ -0,0 +1,60 @@ +/*- + * #%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.statement.select; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Models the SQL Server (T-SQL) {@code OPTION (...)} query hint clause, which attaches a list of + * {@link OptionHint}s to the end of a {@code SELECT}, {@code UPDATE}, {@code DELETE} or + * {@code MERGE} statement, see + * Hints + * (Transact-SQL) - Query Hints. + */ +public class OptionClause implements Serializable { + + private List optionHints; + + public OptionClause() { + super(); + } + + public OptionClause(List optionHints) { + this.optionHints = optionHints; + } + + public List getOptionHints() { + return optionHints; + } + + public void setOptionHints(List optionHints) { + this.optionHints = optionHints; + } + + public OptionClause withOptionHints(List optionHints) { + setOptionHints(optionHints); + return this; + } + + public OptionClause addOptionHint(OptionHint optionHint) { + if (optionHints == null) { + optionHints = new ArrayList<>(); + } + optionHints.add(optionHint); + return this; + } + + @Override + public String toString() { + return " OPTION (" + Select.getStringList(optionHints, true, false) + ")"; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java b/src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java new file mode 100644 index 0000000000..85dc0c12dd --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java @@ -0,0 +1,113 @@ +/*- + * #%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.statement.select; + +import java.io.Serializable; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** + * Models one hint of the SQL Server (T-SQL) {@code OPTION (...)} query hint clause, see + * Hints + * (Transact-SQL) - Query Hints. + *

+ * A hint is a (possibly multi-word) keyword such as {@code RECOMPILE}, {@code HASH JOIN} or + * {@code OPTIMIZE FOR UNKNOWN}, optionally followed by a single value argument ({@code FAST 100}, + * {@code MAXDOP 4}, {@code MAX_GRANT_PERCENT = 25}) or by a parenthesized argument list + * ({@code USE HINT ('...')}, {@code OPTIMIZE FOR (@p = 1)}, {@code TABLE HINT (t, INDEX (i))}). + */ +public class OptionHint implements Serializable { + + private String name; + private Expression value; + private ExpressionList parameters; + private boolean useEquals = false; + + public OptionHint() { + super(); + } + + public OptionHint(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OptionHint withName(String name) { + setName(name); + return this; + } + + public Expression getValue() { + return value; + } + + public void setValue(Expression value) { + this.value = value; + } + + public OptionHint withValue(Expression value) { + setValue(value); + return this; + } + + public ExpressionList getParameters() { + return parameters; + } + + public void setParameters(ExpressionList parameters) { + this.parameters = parameters; + } + + public OptionHint withParameters(ExpressionList parameters) { + setParameters(parameters); + return this; + } + + public OptionHint addParameter(Expression parameter) { + if (parameters == null) { + parameters = new ExpressionList<>(); + } + parameters.add(parameter); + return this; + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + public OptionHint withUseEquals(boolean useEquals) { + setUseEquals(useEquals); + return this; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(name); + if (value != null) { + b.append(useEquals ? " = " : " ").append(value); + } + if (parameters != null) { + b.append(" (").append(Select.getStringList(parameters, true, false)).append(')'); + } + return b.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java b/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java index 38b78384da..f412066c83 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java @@ -9,12 +9,11 @@ */ package net.sf.jsqlparser.statement.select; -import java.io.Serializable; - import java.util.Locale; import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; -public class OrderByElement implements Serializable { +public class OrderByElement extends ASTNodeAccessImpl { private Expression expression; // postgres rollup is an ExpressionList @@ -22,6 +21,7 @@ public class OrderByElement implements Serializable { private boolean asc = true; private boolean ascDescPresent = false; private NullOrdering nullOrdering; + private WithFill withFill; public boolean isAsc() { return asc; @@ -39,6 +39,20 @@ public void setNullOrdering(NullOrdering nullOrdering) { this.nullOrdering = nullOrdering; } + public WithFill getWithFill() { + return withFill; + } + + public OrderByElement setWithFill(WithFill withFill) { + this.withFill = withFill; + return this; + } + + public OrderByElement withWithFill(WithFill withFill) { + setWithFill(withFill); + return this; + } + public boolean isAscDescPresent() { return ascDescPresent; } @@ -74,6 +88,9 @@ public String toString() { b.append(' '); b.append(nullOrdering == NullOrdering.NULLS_FIRST ? "NULLS FIRST" : "NULLS LAST"); } + if (withFill != null) { + b.append(' ').append(withFill); + } if (isMysqlWithRollup()) { b.append(" WITH ROLLUP"); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java b/src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java new file mode 100644 index 0000000000..24f298907b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java @@ -0,0 +1,160 @@ +/*- + * #%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.statement.select; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** + * DuckDB's simplified {@code PIVOT} query. + * + *

+ * Unlike {@link Pivot}, which is a clause attached to a {@link FromItem}, this construct is a query + * in its own right and can therefore be used as a statement, CTE, or parenthesized subquery. + * + * @see DuckDB PIVOT + */ +public class PivotQuery extends Select { + + private FromItem fromItem; + private ExpressionList onExpressions; + private List> usingItems; + private ExpressionList groupByExpressions; + + public FromItem getFromItem() { + return fromItem; + } + + public void setFromItem(FromItem fromItem) { + this.fromItem = fromItem; + } + + public PivotQuery withFromItem(FromItem fromItem) { + setFromItem(fromItem); + return this; + } + + public ExpressionList getOnExpressions() { + return onExpressions; + } + + public void setOnExpressions(ExpressionList onExpressions) { + this.onExpressions = onExpressions; + } + + public PivotQuery withOnExpressions(ExpressionList onExpressions) { + setOnExpressions(onExpressions); + return this; + } + + public PivotQuery addOnExpressions(Expression... expressions) { + return addOnExpressions(Arrays.asList(expressions)); + } + + public PivotQuery addOnExpressions(Collection expressions) { + ExpressionList collection = + Optional.ofNullable(getOnExpressions()).orElseGet(ExpressionList::new); + collection.addAll(expressions); + return withOnExpressions(collection); + } + + public List> getUsingItems() { + return usingItems; + } + + public void setUsingItems(List> usingItems) { + this.usingItems = usingItems; + } + + public PivotQuery withUsingItems(List> usingItems) { + setUsingItems(usingItems); + return this; + } + + public PivotQuery addUsingItems(SelectItem... usingItems) { + List> collection = + Optional.ofNullable(getUsingItems()).orElseGet(ArrayList::new); + Collections.addAll(collection, usingItems); + return withUsingItems(collection); + } + + public PivotQuery addUsingItems(Collection> usingItems) { + List> collection = + Optional.ofNullable(getUsingItems()).orElseGet(ArrayList::new); + collection.addAll(usingItems); + return withUsingItems(collection); + } + + public ExpressionList getGroupByExpressions() { + return groupByExpressions; + } + + public void setGroupByExpressions(ExpressionList groupByExpressions) { + this.groupByExpressions = groupByExpressions; + } + + public PivotQuery withGroupByExpressions(ExpressionList groupByExpressions) { + setGroupByExpressions(groupByExpressions); + return this; + } + + public PivotQuery addGroupByExpressions(Expression... expressions) { + return addGroupByExpressions(Arrays.asList(expressions)); + } + + public PivotQuery addGroupByExpressions(Collection expressions) { + ExpressionList collection = + Optional.ofNullable(getGroupByExpressions()).orElseGet(ExpressionList::new); + collection.addAll(expressions); + return withGroupByExpressions(collection); + } + + @Override + public StringBuilder appendSelectBodyTo(StringBuilder builder) { + builder.append("PIVOT ").append(fromItem); + if (onExpressions != null) { + builder.append(" ON ").append(onExpressions); + } + if (usingItems != null) { + builder.append(" USING ").append(Select.getStringList(usingItems)); + } + if (groupByExpressions != null) { + builder.append(" GROUP BY ").append(groupByExpressions); + } + return builder; + } + + @Override + public T accept(SelectVisitor selectVisitor, S context) { + return selectVisitor.visit(this, context); + } + + @Override + public T accept(FromItemVisitor fromItemVisitor, S context) { + return fromItemVisitor.visit(this, context); + } + + @Override + public SampleClause getSampleClause() { + return null; + } + + @Override + public FromItem setSampleClause(SampleClause sampleClause) { + return null; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/PivotVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/PivotVisitorAdapter.java index 2a9f2c6af2..a8c1047928 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/PivotVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/PivotVisitorAdapter.java @@ -26,19 +26,46 @@ public PivotVisitorAdapter(ExpressionVisitor expressionVisitor) { @Override public T visit(Pivot pivot, S context) { - + if (pivot.getFunctionItems() != null) { + pivot.getFunctionItems() + .forEach(item -> item.getExpression().accept(expressionVisitor, context)); + } + if (pivot.getForColumns() != null) { + pivot.getForColumns().accept(expressionVisitor, context); + } + if (pivot.getSingleInItems() != null) { + pivot.getSingleInItems() + .forEach(item -> item.getExpression().accept(expressionVisitor, context)); + } + if (pivot.getMultiInItems() != null) { + pivot.getMultiInItems() + .forEach(item -> item.getExpression().accept(expressionVisitor, context)); + } return null; } @Override public T visit(PivotXml pivot, S context) { - + visit((Pivot) pivot, context); + if (pivot.getInSelect() != null) { + pivot.getInSelect().accept(expressionVisitor, context); + } return null; } @Override public T visit(UnPivot unpivot, S context) { - + if (unpivot.getUnPivotClause() != null) { + unpivot.getUnPivotClause().forEach(column -> column.accept(expressionVisitor, context)); + } + if (unpivot.getUnPivotForClause() != null) { + unpivot.getUnPivotForClause() + .forEach(column -> column.accept(expressionVisitor, context)); + } + if (unpivot.getUnPivotInClause() != null) { + unpivot.getUnPivotInClause() + .forEach(item -> item.getExpression().accept(expressionVisitor, context)); + } return null; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java b/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java index 1d76255e05..ea83b6d5ee 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java @@ -15,9 +15,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.Set; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.OracleHierarchicalExpression; @@ -43,19 +45,20 @@ public class PlainSelect extends Select { private GroupByElement groupBy; private Expression having; private Expression qualify; + private boolean qualifyAfterWindow; private OptimizeFor optimizeFor; private Skip skip; - private boolean mySqlHintStraightJoin; + private final EnumSet mySqlSelectModifiers = + EnumSet.noneOf(MySqlSelectModifier.class); private First first; private Top top; private OracleHierarchicalExpression oracleHierarchical = null; private PreferringClause preferringClause = null; private OracleHint oracleHint = null; - private boolean mySqlSqlCalcFoundRows = false; - private MySqlSqlCacheFlags mySqlCacheFlag = null; private String forXmlPath; private KSQLWindow ksqlWindow = null; - private boolean emitChanges = false; + private EmitMode emitMode = EmitMode.NONE; + private List windowDefinitions; /** * @see settings = null; + public enum EmitMode { + NONE, CHANGES, FINAL + } + public PlainSelect() {} public PlainSelect(FromItem fromItem) { @@ -391,11 +399,11 @@ public void setSkip(Skip skip) { } public boolean getMySqlHintStraightJoin() { - return this.mySqlHintStraightJoin; + return mySqlSelectModifiers.contains(MySqlSelectModifier.STRAIGHT_JOIN); } public void setMySqlHintStraightJoin(boolean mySqlHintStraightJoin) { - this.mySqlHintStraightJoin = mySqlHintStraightJoin; + setMySqlSelectModifier(MySqlSelectModifier.STRAIGHT_JOIN, mySqlHintStraightJoin); } public First getFirst() { @@ -435,6 +443,16 @@ public Expression getQualify() { return qualify; } + /** Whether QUALIFY follows the WINDOW clause, as in DuckDB, rather than preceding it. */ + public boolean isQualifyAfterWindow() { + return qualifyAfterWindow; + } + + public PlainSelect setQualifyAfterWindow(boolean qualifyAfterWindow) { + this.qualifyAfterWindow = qualifyAfterWindow; + return this; + } + public PlainSelect setQualify(Expression qualify) { this.qualify = qualify; return this; @@ -500,12 +518,32 @@ public void setKsqlWindow(KSQLWindow ksqlWindow) { this.ksqlWindow = ksqlWindow; } + public EmitMode getEmitMode() { + return emitMode; + } + + public void setEmitMode(EmitMode emitMode) { + this.emitMode = java.util.Objects.requireNonNull(emitMode, "emitMode"); + } + + public PlainSelect withEmitMode(EmitMode emitMode) { + setEmitMode(emitMode); + return this; + } + + public StringBuilder appendEmitClauseTo(StringBuilder builder) { + if (emitMode != EmitMode.NONE) { + builder.append(" EMIT ").append(emitMode); + } + return builder; + } + public boolean isEmitChanges() { - return emitChanges; + return emitMode == EmitMode.CHANGES; } public void setEmitChanges(boolean emitChanges) { - this.emitChanges = emitChanges; + emitMode = emitChanges ? EmitMode.CHANGES : EmitMode.NONE; } public List getWindowDefinitions() { @@ -518,13 +556,92 @@ public void setWindowDefinitions(List windowDefinitions) { @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveMethodLength", "PMD.NPathComplexity"}) + /** + * DuckDB allows the FROM clause to come first, with the SELECT clause optional: + * {@code FROM tbl SELECT a} and {@code FROM tbl WHERE a > 1}. + */ + public boolean isFromFirst() { + return fromFirst; + } + + public void setFromFirst(boolean fromFirst) { + this.fromFirst = fromFirst; + } + + public PlainSelect withFromFirst(boolean fromFirst) { + setFromFirst(fromFirst); + return this; + } + public StringBuilder appendSelectBodyTo(StringBuilder builder) { - builder.append("SELECT "); + if (fromFirst) { + // DuckDB allows the FROM clause to lead: FROM tbl [SELECT ...] [WHERE ...] + appendFromClauseTo(builder); + if (selectItems != null && !selectItems.isEmpty()) { + builder.append(" "); + appendSelectClauseTo(builder); + appendIntoClausesTo(builder); + } + } else { + appendSelectClauseTo(builder); + appendIntoClausesTo(builder); + appendFromClauseTo(builder); + } + + if (preWhere != null) { + builder.append(" PREWHERE ").append(preWhere); + } + if (where != null) { + builder.append(" WHERE ").append(where); + } + if (oracleHierarchical != null) { + builder.append(oracleHierarchical); + } + if (preferringClause != null) { + builder.append(" ").append(preferringClause); + } + if (groupBy != null) { + builder.append(" ").append(groupBy); + } + if (having != null) { + builder.append(" HAVING ").append(having); + } + if (qualify != null && !qualifyAfterWindow) { + builder.append(" QUALIFY ").append(qualify); + } + if (windowDefinitions != null) { + builder.append(" WINDOW "); + builder.append(windowDefinitions.stream().map(WindowDefinition::toString) + .collect(joining(", "))); + } + if (qualify != null && qualifyAfterWindow) { + builder.append(" QUALIFY ").append(qualify); + } + appendEmitClauseTo(builder); + if (intoTempTable != null) { + builder.append(" INTO TEMP ").append(intoTempTable); + } + if (useWithNoLog) { + builder.append(" WITH NO LOG"); + } + return builder; + } - if (this.mySqlHintStraightJoin) { - builder.append("STRAIGHT_JOIN "); + private void appendSelectClauseTo(StringBuilder builder) { + builder.append("SELECT "); + int afterSelect = builder.length(); + appendSelectHintsTo(builder); + appendSelectQualifiersTo(builder); + appendMySqlSelectModifiersTo(builder); + builder.append(getStringList(selectItems)); + // PostgreSQL permits SELECT with no target expressions: keep "SELECT + // FROM" instead of doubling the space. + if (builder.length() == afterSelect) { + builder.deleteCharAt(afterSelect - 1); } + } + private void appendSelectHintsTo(StringBuilder builder) { if (oracleHint != null) { builder.append(oracleHint).append(" "); } @@ -536,7 +653,9 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { if (first != null) { builder.append(first).append(" "); } + } + private void appendSelectQualifiersTo(StringBuilder builder) { if (distinct != null) { builder.append(distinct).append(" "); } @@ -555,14 +674,54 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { if (top != null) { builder.append(top).append(" "); } - if (mySqlCacheFlag != null) { - builder.append(mySqlCacheFlag.name()).append(" "); + } + + /** Appends the typed modifiers in MySQL's documented SELECT order. */ + public void appendMySqlSelectModifiersTo(StringBuilder builder) { + for (MySqlSelectModifier modifier : mySqlSelectModifiers) { + builder.append(modifier).append(" "); + } + } + + /** Returns the modifiers independently of the selected expressions and aliases. */ + public Set getMySqlSelectModifiers() { + return Collections.unmodifiableSet(mySqlSelectModifiers); + } + + public void setMySqlSelectModifiers(Collection modifiers) { + EnumSet copy = EnumSet.noneOf(MySqlSelectModifier.class); + if (modifiers != null) { + copy.addAll(modifiers); + } + if (copy.contains(MySqlSelectModifier.SQL_CACHE) + && copy.contains(MySqlSelectModifier.SQL_NO_CACHE)) { + throw new IllegalArgumentException("SQL_CACHE and SQL_NO_CACHE cannot be combined"); } - if (mySqlSqlCalcFoundRows) { - builder.append("SQL_CALC_FOUND_ROWS").append(" "); + mySqlSelectModifiers.clear(); + mySqlSelectModifiers.addAll(copy); + } + + public PlainSelect addMySqlSelectModifiers(MySqlSelectModifier... modifiers) { + EnumSet copy = mySqlSelectModifiers.clone(); + Collections.addAll(copy, modifiers); + setMySqlSelectModifiers(copy); + return this; + } + + public PlainSelect withMySqlSelectModifiers(Collection modifiers) { + setMySqlSelectModifiers(modifiers); + return this; + } + + private void setMySqlSelectModifier(MySqlSelectModifier modifier, boolean enabled) { + if (enabled) { + mySqlSelectModifiers.add(modifier); + } else { + mySqlSelectModifiers.remove(modifier); } - builder.append(getStringList(selectItems)); + } + private void appendIntoClausesTo(StringBuilder builder) { if (intoTables != null) { builder.append(" INTO "); for (Iterator

iter = intoTables.iterator(); iter.hasNext();) { @@ -578,9 +737,11 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { .getPosition() == MySqlSelectIntoClause.Position.BEFORE_FROM) { builder.append(" ").append(mySqlSelectIntoClause); } + } + private void appendFromClauseTo(StringBuilder builder) { if (fromItem != null) { - builder.append(" FROM "); + builder.append(fromFirst ? "FROM " : " FROM "); if (isUsingOnly) { builder.append("ONLY "); } @@ -607,53 +768,10 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { if (ksqlWindow != null) { builder.append(" WINDOW ").append(ksqlWindow); } - if (preWhere != null) { - builder.append(" PREWHERE ").append(preWhere); - } - if (where != null) { - builder.append(" WHERE ").append(where); - } - if (oracleHierarchical != null) { - builder.append(oracleHierarchical); - } - if (preferringClause != null) { - builder.append(" ").append(preferringClause); - } - if (groupBy != null) { - builder.append(" ").append(groupBy); - } - if (having != null) { - builder.append(" HAVING ").append(having); - } - if (qualify != null) { - builder.append(" QUALIFY ").append(qualify); - } - if (windowDefinitions != null) { - builder.append(" WINDOW "); - builder.append(windowDefinitions.stream().map(WindowDefinition::toString) - .collect(joining(", "))); - } - if (emitChanges) { - builder.append(" EMIT CHANGES"); - } - } else { - // without from - if (preWhere != null) { - builder.append(" PREWHERE ").append(preWhere); - } - if (where != null) { - builder.append(" WHERE ").append(where); - } - } - if (intoTempTable != null) { - builder.append(" INTO TEMP ").append(intoTempTable); - } - if (useWithNoLog) { - builder.append(" WITH NO LOG"); } - return builder; } + @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveMethodLength", "PMD.NPathComplexity"}) @@ -693,19 +811,28 @@ public PlainSelect withMySqlSqlNoCache(MySqlSqlCacheFlags mySqlCacheFlag) { } public boolean getMySqlSqlCalcFoundRows() { - return this.mySqlSqlCalcFoundRows; + return mySqlSelectModifiers.contains(MySqlSelectModifier.SQL_CALC_FOUND_ROWS); } public void setMySqlSqlCalcFoundRows(boolean mySqlCalcFoundRows) { - this.mySqlSqlCalcFoundRows = mySqlCalcFoundRows; + setMySqlSelectModifier(MySqlSelectModifier.SQL_CALC_FOUND_ROWS, mySqlCalcFoundRows); } public MySqlSqlCacheFlags getMySqlSqlCacheFlag() { - return this.mySqlCacheFlag; + if (mySqlSelectModifiers.contains(MySqlSelectModifier.SQL_CACHE)) { + return MySqlSqlCacheFlags.SQL_CACHE; + } + return mySqlSelectModifiers.contains(MySqlSelectModifier.SQL_NO_CACHE) + ? MySqlSqlCacheFlags.SQL_NO_CACHE + : null; } public void setMySqlSqlCacheFlag(MySqlSqlCacheFlags sqlCacheFlag) { - this.mySqlCacheFlag = sqlCacheFlag; + mySqlSelectModifiers.remove(MySqlSelectModifier.SQL_CACHE); + mySqlSelectModifiers.remove(MySqlSelectModifier.SQL_NO_CACHE); + if (sqlCacheFlag != null) { + mySqlSelectModifiers.add(MySqlSelectModifier.valueOf(sqlCacheFlag.name())); + } } public PlainSelect withDistinct(Distinct distinct) { diff --git a/src/main/java/net/sf/jsqlparser/statement/select/RowPattern.java b/src/main/java/net/sf/jsqlparser/statement/select/RowPattern.java new file mode 100644 index 0000000000..9d922ff5b3 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/RowPattern.java @@ -0,0 +1,392 @@ +/*- + * #%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.statement.select; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** A row pattern, with explicit grouping and ordered alternatives preserved. */ +public abstract class RowPattern extends ASTNodeAccessImpl { + public abstract T accept(RowPatternVisitor visitor, S context); + + public abstract StringBuilder appendTo(StringBuilder builder, Consumer expressions); + + /** Visits only SQL expressions embedded in the pattern, such as quantifier bounds. */ + public void forEachExpression(Consumer expressions) {} + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, expression -> builder.append(expression)).toString(); + } + + public static class Variable extends RowPattern { + private String name; + + public Variable(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public Variable setName(String name) { + this.name = name; + return this; + } + + @Override + public T accept(RowPatternVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + return builder.append(name); + } + } + + public static class Empty extends RowPattern { + @Override + public T accept(RowPatternVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + return builder; + } + } + + public static class Anchor extends RowPattern { + public enum Type { + START, END + } + + private Type type; + + public Anchor(Type type) { + this.type = type; + } + + public Type getType() { + return type; + } + + public Anchor setType(Type type) { + this.type = type; + return this; + } + + @Override + public T accept(RowPatternVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + return builder.append(type == Type.START ? '^' : '$'); + } + } + + public static class Group extends RowPattern { + private RowPattern pattern; + + public Group(RowPattern pattern) { + this.pattern = pattern; + } + + public RowPattern getPattern() { + return pattern; + } + + public Group setPattern(RowPattern pattern) { + this.pattern = pattern; + return this; + } + + @Override + public T accept(RowPatternVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public void forEachExpression(Consumer expressions) { + pattern.forEachExpression(expressions); + } + + @Override + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + builder.append('('); + pattern.appendTo(builder, expressions); + return builder.append(')'); + } + } + + public static class Operation extends RowPattern { + public enum Type { + SEQUENCE, ALTERNATION + } + + private Type type; + private List patterns; + + public Operation(Type type, List patterns) { + this.type = type; + this.patterns = new ArrayList<>(patterns); + } + + public Type getType() { + return type; + } + + public Operation setType(Type type) { + this.type = type; + return this; + } + + public List getPatterns() { + return patterns; + } + + public Operation setPatterns(List patterns) { + this.patterns = patterns; + return this; + } + + @Override + public T accept(RowPatternVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public void forEachExpression(Consumer expressions) { + patterns.forEach(pattern -> pattern.forEachExpression(expressions)); + } + + @Override + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + for (int i = 0; i < patterns.size(); i++) { + if (i > 0) { + builder.append(type == Type.SEQUENCE ? " " : " | "); + } + RowPattern child = patterns.get(i); + // Explicit parentheses keep the tree unambiguous across dialects, including + // Snowflake's documented alternative-before-concatenation precedence. + boolean brackets = type == Type.SEQUENCE && child instanceof Empty + || child instanceof Operation && ((Operation) child).type != type; + if (brackets) { + builder.append('('); + } + child.appendTo(builder, expressions); + if (brackets) { + builder.append(')'); + } + } + return builder; + } + } + + /** A permutation stays compact; it is never expanded into factorially many alternatives. */ + public static class Permute extends RowPattern { + private List patterns; + + public Permute(List patterns) { + this.patterns = new ArrayList<>(patterns); + } + + public List getPatterns() { + return patterns; + } + + public Permute setPatterns(List patterns) { + this.patterns = patterns; + return this; + } + + @Override + public T accept(RowPatternVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public void forEachExpression(Consumer expressions) { + patterns.forEach(pattern -> pattern.forEachExpression(expressions)); + } + + @Override + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + builder.append("PERMUTE("); + for (int i = 0; i < patterns.size(); i++) { + if (i > 0) { + builder.append(", "); + } + patterns.get(i).appendTo(builder, expressions); + } + return builder.append(')'); + } + } + + public static class Exclusion extends RowPattern { + private RowPattern pattern; + + public Exclusion(RowPattern pattern) { + this.pattern = pattern; + } + + public RowPattern getPattern() { + return pattern; + } + + public Exclusion setPattern(RowPattern pattern) { + this.pattern = pattern; + return this; + } + + @Override + public T accept(RowPatternVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public void forEachExpression(Consumer expressions) { + pattern.forEachExpression(expressions); + } + + @Override + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + builder.append("{- "); + pattern.appendTo(builder, expressions); + return builder.append(" -}"); + } + } + + public static class Quantified extends RowPattern { + public enum Type { + ZERO_OR_MORE, ONE_OR_MORE, OPTIONAL, EXACT, RANGE + } + + private RowPattern pattern; + private Type type; + private Expression lowerBound; + private Expression upperBound; + private boolean reluctant; + + public Quantified(RowPattern pattern, Type type) { + this.pattern = pattern; + this.type = type; + } + + public RowPattern getPattern() { + return pattern; + } + + public Quantified setPattern(RowPattern pattern) { + this.pattern = pattern; + return this; + } + + public Type getType() { + return type; + } + + public Quantified setType(Type type) { + this.type = type; + return this; + } + + public Expression getLowerBound() { + return lowerBound; + } + + public Quantified setLowerBound(Expression bound) { + lowerBound = bound; + return this; + } + + public Expression getUpperBound() { + return upperBound; + } + + public Quantified setUpperBound(Expression bound) { + upperBound = bound; + return this; + } + + public boolean isReluctant() { + return reluctant; + } + + public Quantified setReluctant(boolean reluctant) { + this.reluctant = reluctant; + return this; + } + + @Override + public T accept(RowPatternVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public void forEachExpression(Consumer expressions) { + pattern.forEachExpression(expressions); + if (lowerBound != null) { + expressions.accept(lowerBound); + } + if (upperBound != null) { + expressions.accept(upperBound); + } + } + + @Override + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + boolean brackets = !(pattern instanceof Variable || pattern instanceof Group + || pattern instanceof Permute || pattern instanceof Exclusion); + if (brackets) { + builder.append('('); + } + pattern.appendTo(builder, expressions); + if (brackets) { + builder.append(')'); + } + switch (type) { + case ZERO_OR_MORE: + builder.append('*'); + break; + case ONE_OR_MORE: + builder.append('+'); + break; + case OPTIONAL: + builder.append('?'); + break; + default: + builder.append('{'); + if (lowerBound != null) { + expressions.accept(lowerBound); + } + if (type == Type.RANGE) { + builder.append(','); + if (upperBound != null) { + expressions.accept(upperBound); + } + } + builder.append('}'); + } + if (reluctant) { + builder.append('?'); + } + return builder; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/RowPatternVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/RowPatternVisitor.java new file mode 100644 index 0000000000..e61bf4ad92 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/RowPatternVisitor.java @@ -0,0 +1,29 @@ +/*- + * #%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.statement.select; + +/** Visitor for the structural operators of a row pattern. */ +public interface RowPatternVisitor { + T visit(RowPattern.Variable pattern, S context); + + T visit(RowPattern.Empty pattern, S context); + + T visit(RowPattern.Anchor pattern, S context); + + T visit(RowPattern.Group pattern, S context); + + T visit(RowPattern.Operation pattern, S context); + + T visit(RowPattern.Quantified pattern, S context); + + T visit(RowPattern.Permute pattern, S context); + + T visit(RowPattern.Exclusion pattern, S context); +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/RowPatternVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/RowPatternVisitorAdapter.java new file mode 100644 index 0000000000..65241f984f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/RowPatternVisitorAdapter.java @@ -0,0 +1,80 @@ +/*- + * #%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.statement.select; + +import net.sf.jsqlparser.expression.ExpressionVisitor; + +/** Traverses pattern children and, when supplied, SQL expressions in quantifier bounds. */ +public class RowPatternVisitorAdapter implements RowPatternVisitor { + private final ExpressionVisitor expressionVisitor; + + public RowPatternVisitorAdapter() { + this(null); + } + + public RowPatternVisitorAdapter(ExpressionVisitor expressionVisitor) { + this.expressionVisitor = expressionVisitor; + } + + @Override + public T visit(RowPattern.Variable pattern, S context) { + return null; + } + + @Override + public T visit(RowPattern.Empty pattern, S context) { + return null; + } + + @Override + public T visit(RowPattern.Anchor pattern, S context) { + return null; + } + + @Override + public T visit(RowPattern.Group pattern, S context) { + return pattern.getPattern().accept(this, context); + } + + @Override + public T visit(RowPattern.Operation pattern, S context) { + for (RowPattern child : pattern.getPatterns()) { + child.accept(this, context); + } + return null; + } + + @Override + public T visit(RowPattern.Quantified pattern, S context) { + pattern.getPattern().accept(this, context); + if (expressionVisitor != null) { + if (pattern.getLowerBound() != null) { + pattern.getLowerBound().accept(expressionVisitor, context); + } + if (pattern.getUpperBound() != null) { + pattern.getUpperBound().accept(expressionVisitor, context); + } + } + return null; + } + + @Override + public T visit(RowPattern.Permute pattern, S context) { + for (RowPattern child : pattern.getPatterns()) { + child.accept(this, context); + } + return null; + } + + @Override + public T visit(RowPattern.Exclusion pattern, S context) { + return pattern.getPattern().accept(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SampleClause.java b/src/main/java/net/sf/jsqlparser/statement/select/SampleClause.java index e6bc8f21d9..0eb923938c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SampleClause.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SampleClause.java @@ -9,14 +9,16 @@ */ package net.sf.jsqlparser.statement.select; +import java.io.Serializable; import java.util.Locale; -public class SampleClause { +public class SampleClause implements Serializable { private SampleKeyword keyword; private SampleMethod method; private Number percentageArgument; private String percentageUnit; private boolean argumentInBrackets = true; + private boolean methodInBrackets = false; // ClickHouse specific private Number offsetArgument; private Number repeatArgument; @@ -130,10 +132,23 @@ public SampleClause setMethod(String method) { return this; } + /** + * DuckDB writes the sampling method behind the sample size: + * {@code USING SAMPLE 10 PERCENT (bernoulli)}. + */ + public boolean isMethodInBrackets() { + return methodInBrackets; + } + + public SampleClause setMethodInBrackets(boolean methodInBrackets) { + this.methodInBrackets = methodInBrackets; + return this; + } + public StringBuilder appendTo(StringBuilder builder) { builder.append(" ").append(keyword); - if (method != null) { + if (method != null && !methodInBrackets) { builder.append(" ").append(method); } @@ -144,11 +159,20 @@ public StringBuilder appendTo(StringBuilder builder) { } else { builder.append(" ").append(percentageArgument); if (percentageUnit != null) { - builder.append(" ").append(percentageUnit); + // "10%" has no blank, "10 PERCENT" and "10 ROWS" do + builder.append("%".equals(percentageUnit) ? "" : " ").append(percentageUnit); } } } + if (methodInBrackets) { + builder.append(" (").append(method); + if (seedArgument != null) { + builder.append(", ").append(seedArgument); + } + builder.append(")"); + } + if (offsetArgument != null) { builder.append(" OFFSET ").append(offsetArgument); } @@ -157,7 +181,7 @@ public StringBuilder appendTo(StringBuilder builder) { builder.append(" REPEATABLE (").append(repeatArgument).append(")"); } - if (seedArgument != null) { + if (seedArgument != null && !methodInBrackets) { builder.append(" SEED (").append(seedArgument).append(")"); } @@ -189,7 +213,7 @@ public String toString() { } public enum SampleMethod { - BERNOULLI, SYSTEM, BLOCK; + BERNOULLI, SYSTEM, BLOCK, RESERVOIR; public static SampleMethod from(String sampleMethod) { return Enum.valueOf(SampleMethod.class, diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Select.java b/src/main/java/net/sf/jsqlparser/statement/select/Select.java index 737608020f..2dd82f93ea 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Select.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Select.java @@ -31,11 +31,14 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp Offset offset; Fetch fetch; WithIsolation isolation; + MySqlProcedureAnalyse mySqlProcedureAnalyse; boolean oracleSiblings = false; ForClause forClause = null; + OptionClause option = null; List orderByElements; + List interpolate; ForMode forMode = null; private boolean skipLocked; private Wait wait; @@ -54,6 +57,16 @@ public static String orderByToString(boolean oracleSiblings, return getFormattedList(orderByElements, oracleSiblings ? "ORDER SIBLINGS BY" : "ORDER BY"); } + public static String interpolateToString(List interpolate) { + if (interpolate == null) { + return ""; + } + if (interpolate.isEmpty()) { + return " INTERPOLATE"; + } + return getFormattedList(interpolate, "INTERPOLATE", true, true); + } + public static String getFormattedList(List list, String expression) { return getFormattedList(list, expression, true, false); } @@ -118,9 +131,13 @@ public static StringBuilder appendStringListTo(StringBuilder builder, List li builder.append("("); } - int size = list.size(); - for (int i = 0; i < size; i++) { - builder.append(list.get(i)).append(i < size - 1 ? comma : ""); + boolean first = true; + for (Object element : list) { + if (!first) { + builder.append(comma); + } + builder.append(element); + first = false; } if (useBrackets) { @@ -184,6 +201,19 @@ public Select setForClause(ForClause forClause) { return this; } + public OptionClause getOption() { + return option; + } + + public Select setOption(OptionClause option) { + this.option = option; + return this; + } + + public Select withOption(OptionClause option) { + return setOption(option); + } + public List getOrderByElements() { return orderByElements; } @@ -192,6 +222,19 @@ public void setOrderByElements(List orderByElements) { this.orderByElements = orderByElements; } + public List getInterpolate() { + return interpolate; + } + + public void setInterpolate(List interpolate) { + this.interpolate = interpolate; + } + + public Select withInterpolate(List interpolate) { + setInterpolate(interpolate); + return this; + } + public Select withOrderByElements(List orderByElements) { this.setOrderByElements(orderByElements); return this; @@ -284,6 +327,19 @@ public Select withIsolation(WithIsolation isolation) { return this; } + public MySqlProcedureAnalyse getMySqlProcedureAnalyse() { + return mySqlProcedureAnalyse; + } + + public void setMySqlProcedureAnalyse(MySqlProcedureAnalyse mySqlProcedureAnalyse) { + this.mySqlProcedureAnalyse = mySqlProcedureAnalyse; + } + + public Select withMySqlProcedureAnalyse(MySqlProcedureAnalyse mySqlProcedureAnalyse) { + setMySqlProcedureAnalyse(mySqlProcedureAnalyse); + return this; + } + public ForMode getForMode() { return this.forMode; } @@ -463,12 +519,17 @@ public StringBuilder appendTo(StringBuilder builder) { if (!forUpdateBeforeOrderBy) { builder.append(orderByToString(oracleSiblings, orderByElements)); + builder.append(interpolateToString(interpolate)); } if (forClause != null) { forClause.appendTo(builder); } + if (option != null) { + builder.append(option); + } + if (limitBy != null) { builder.append(limitBy); } @@ -478,6 +539,9 @@ public StringBuilder appendTo(StringBuilder builder) { if (offset != null) { builder.append(offset); } + if (mySqlProcedureAnalyse != null) { + mySqlProcedureAnalyse.appendTo(builder); + } if (fetch != null) { builder.append(fetch); } @@ -512,6 +576,7 @@ public StringBuilder appendTo(StringBuilder builder) { if (forUpdateBeforeOrderBy) { builder.append(orderByToString(oracleSiblings, orderByElements)); + builder.append(interpolateToString(interpolate)); } return builder; diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java index db5ce31752..5591f4bf83 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java @@ -9,11 +9,10 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.List; import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.piped.FromQuery; -import java.util.List; - public interface SelectVisitor { default T visitWithItems(List> withItemsList, S context) { if (withItemsList != null) { @@ -40,6 +39,22 @@ default void visit(PlainSelect plainSelect) { this.visit(plainSelect, null); } + default T visit(PivotQuery pivotQuery, S context) { + return null; + } + + default void visit(PivotQuery pivotQuery) { + this.visit(pivotQuery, null); + } + + default T visit(UnPivotQuery unPivotQuery, S context) { + return null; + } + + default void visit(UnPivotQuery unPivotQuery) { + this.visit(unPivotQuery, null); + } + T visit(FromQuery fromQuery, S context); T visit(SetOperationList setOpList, S context); diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java index 9b0eb33a65..8b6e657875 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -9,9 +9,16 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.List; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.WindowDefinition; import net.sf.jsqlparser.statement.OutputClause; +import net.sf.jsqlparser.statement.ParenthesedStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; import net.sf.jsqlparser.statement.piped.FromQuery; @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) @@ -21,6 +28,14 @@ public class SelectVisitorAdapter implements SelectVisitor { private final SelectItemVisitor selectItemVisitor; private final FromItemVisitor fromItemVisitor; + /** + * Optional, and only needed for data-modifying CTEs - see {@link #visit(WithItem, Object)}. A + * plain SelectVisitor cannot accept an INSERT/UPDATE/DELETE body; without this the CTE is + * skipped rather than traversed. {@link net.sf.jsqlparser.statement.StatementVisitorAdapter} + * sets it automatically. + */ + private StatementVisitor statementVisitor; + public SelectVisitorAdapter() { this.expressionVisitor = new ExpressionVisitorAdapter<>(this); this.pivotVisitor = new PivotVisitorAdapter<>(this.expressionVisitor); @@ -49,17 +64,13 @@ public SelectVisitorAdapter(ExpressionVisitor expressionVisitor) { this.expressionVisitor = expressionVisitor; this.pivotVisitor = new PivotVisitorAdapter<>(); this.selectItemVisitor = new SelectItemVisitorAdapter<>(this.expressionVisitor); - this.fromItemVisitor = new FromItemVisitorAdapter<>(); + this.fromItemVisitor = new FromItemVisitorAdapter<>(this, this.expressionVisitor); } @Override public T visitOutputClause(OutputClause outputClause, S context) { if (outputClause != null) { - if (outputClause.getSelectItemList() != null) { - for (SelectItem selectItem : outputClause.getSelectItemList()) { - selectItem.accept(selectItemVisitor, context); - } - } + visitSelectItems(outputClause.getSelectItemList(), context); if (outputClause.getTableVariable() != null) { outputClause.getTableVariable().accept(expressionVisitor, context); } @@ -74,6 +85,14 @@ public T visitOutputClause(OutputClause outputClause, S context) { return null; } + private void visitSelectItems(List> items, S context) { + if (items != null) { + for (SelectItem item : items) { + item.accept(selectItemVisitor, context); + } + } + } + public ExpressionVisitor getExpressionVisitor() { return expressionVisitor; } @@ -90,6 +109,15 @@ public FromItemVisitor getFromItemVisitor() { return fromItemVisitor; } + public StatementVisitor getStatementVisitor() { + return statementVisitor; + } + + public SelectVisitorAdapter setStatementVisitor(StatementVisitor statementVisitor) { + this.statementVisitor = statementVisitor; + return this; + } + @Override public T visit(ParenthesedSelect select, S context) { visitWithItems(select.withItemsList, context); @@ -107,16 +135,19 @@ public T visit(ParenthesedSelect select, S context) { unpivot.accept(pivotVisitor, context); } - expressionVisitor.visitLimit(select.getLimit(), context); + visitLimitOffsetFetch(select, context); + return null; + } + private void visitLimitOffsetFetch(Select select, S context) { + expressionVisitor.visitLimit(select.getLimitBy(), context); + expressionVisitor.visitLimit(select.getLimit(), context); if (select.getOffset() != null) { - expressionVisitor.visitExpression(select.getOffset().getOffset(), null); + expressionVisitor.visitExpression(select.getOffset().getOffset(), context); } if (select.getFetch() != null) { - expressionVisitor.visitExpression(select.getFetch().getExpression(), null); + expressionVisitor.visitExpression(select.getFetch().getExpression(), context); } - - return null; } @Override @@ -125,21 +156,18 @@ public T visit(PlainSelect plainSelect, S context) { visitWithItems(plainSelect.withItemsList, context); if (plainSelect.getDistinct() != null) { - for (SelectItem selectItem : plainSelect.getDistinct().getOnSelectItems()) { - selectItem.accept(selectItemVisitor, context); - } + visitSelectItems(plainSelect.getDistinct().getOnSelectItems(), context); } if (plainSelect.getTop() != null) { plainSelect.getTop().getExpression().accept(expressionVisitor, context); } - for (SelectItem selectItem : plainSelect.getSelectItems()) { - selectItem.accept(selectItemVisitor, context); - } + visitSelectItems(plainSelect.getSelectItems(), context); if (plainSelect.getMySqlSelectIntoClause() != null) { MySqlSelectIntoClause mySqlSelectIntoClause = plainSelect.getMySqlSelectIntoClause(); + expressionVisitor.visitExpressions(mySqlSelectIntoClause.getVariables(), context); expressionVisitor.visitExpression(mySqlSelectIntoClause.getFileName(), context); expressionVisitor.visitExpression(mySqlSelectIntoClause.getFieldsTerminatedBy(), context); @@ -165,20 +193,20 @@ public T visit(PlainSelect plainSelect, S context) { expressionVisitor.visitExpression(plainSelect.getPreWhere(), context); expressionVisitor.visitExpression(plainSelect.getWhere(), context); - - // if (plainSelect.getOracleHierarchical() != null) { - // //@todo: implement - // } - // + expressionVisitor.visitExpression(plainSelect.getOracleHierarchical(), context); expressionVisitor.visitPreferringClause(plainSelect.getPreferringClause(), context); expressionVisitor.visit(plainSelect.getGroupBy(), context); expressionVisitor.visitExpression(plainSelect.getHaving(), context); expressionVisitor.visitExpression(plainSelect.getQualify(), context); - // if (plainSelect.getWindowDefinitions() != null) { - // //@todo: implement - // } + if (plainSelect.getWindowDefinitions() != null) { + for (WindowDefinition window : plainSelect.getWindowDefinitions()) { + for (Expression expression : window.getAllExpressions()) { + expressionVisitor.visitExpression(expression, context); + } + } + } Pivot pivot = plainSelect.getPivot(); if (pivot != null) { @@ -190,18 +218,13 @@ public T visit(PlainSelect plainSelect, S context) { } expressionVisitor.visitOrderBy(plainSelect.getOrderByElements(), context); + visitLimitOffsetFetch(plainSelect, context); - // if (plainSelect.getLimitBy() != null) { - // //@todo: implement - // } - // if (plainSelect.getLimit() != null) { - // //@todo: implement - // } - if (plainSelect.getOffset() != null) { - expressionVisitor.visitExpression(plainSelect.getOffset().getOffset(), context); - } - if (plainSelect.getFetch() != null) { - expressionVisitor.visitExpression(plainSelect.getFetch().getExpression(), context); + if (plainSelect.getMySqlProcedureAnalyse() != null) { + expressionVisitor.visitExpression( + plainSelect.getMySqlProcedureAnalyse().getMaxElements(), context); + expressionVisitor.visitExpression( + plainSelect.getMySqlProcedureAnalyse().getMaxMemory(), context); } // if (plainSelect.getForMode() != null) { // //@todo: implement @@ -211,6 +234,42 @@ public T visit(PlainSelect plainSelect, S context) { return null; } + @Override + public T visit(PivotQuery pivotQuery, S context) { + visitWithItems(pivotQuery.getWithItemsList(), context); + + fromItemVisitor.visitFromItem(pivotQuery.getFromItem(), context); + expressionVisitor.visitExpressions(pivotQuery.getOnExpressions(), context); + + if (pivotQuery.getUsingItems() != null) { + for (SelectItem item : pivotQuery.getUsingItems()) { + item.accept(selectItemVisitor, context); + } + } + + expressionVisitor.visitExpressions(pivotQuery.getGroupByExpressions(), context); + expressionVisitor.visitOrderBy(pivotQuery.getOrderByElements(), context); + expressionVisitor.visitLimit(pivotQuery.getLimit(), context); + + if (pivotQuery.getOffset() != null) { + expressionVisitor.visitExpression(pivotQuery.getOffset().getOffset(), context); + } + if (pivotQuery.getFetch() != null) { + expressionVisitor.visitExpression(pivotQuery.getFetch().getExpression(), context); + } + return null; + } + + @Override + public T visit(UnPivotQuery unPivotQuery, S context) { + visitWithItems(unPivotQuery.getWithItemsList(), context); + + fromItemVisitor.visitFromItem(unPivotQuery.getFromItem(), context); + expressionVisitor.visitExpressions(unPivotQuery.getOnExpressions(), context); + expressionVisitor.visitExpressions(unPivotQuery.getValueColumns(), context); + return null; + } + @Override public T visit(FromQuery fromQuery, S context) { return null; @@ -218,19 +277,66 @@ public T visit(FromQuery fromQuery, S context) { @Override public T visit(SetOperationList setOpList, S context) { + visitWithItems(setOpList.getWithItemsList(), context); for (Select select : setOpList.getSelects()) { select.accept(this, context); } + expressionVisitor.visitOrderBy(setOpList.getOrderByElements(), context); + visitLimitOffsetFetch(setOpList, context); return null; } + /** + * A CTE body is a {@link ParenthesedStatement}, which is not necessarily a SELECT: PostgreSQL + * and others allow {@code WITH c AS (DELETE FROM t RETURNING *) SELECT * FROM c}. + * + *

+ * The previous implementation called {@code withItem.getSelect()}, an unchecked cast to + * {@link ParenthesedSelect}, and threw {@link ClassCastException} on every data-modifying CTE + * reached through the select path - which {@code ExpressionVisitorAdapter#visit(Select)} does + * for any sub-select. {@code WithItem#accept(StatementVisitor)} has always dispatched correctly + * through {@code getParenthesedStatement()}; only this path did not. + */ @Override public T visit(WithItem withItem, S context) { - return withItem.getSelect().accept(this, context); + if (withItem.getCycleClause() != null) { + withItem.getCycleClause().accept(expressionVisitor, context); + } + ParenthesedStatement body = withItem.getParenthesedStatement(); + + // ParenthesedSelect is a Select and stays on the select path + if (body instanceof Select) { + return ((Select) body).accept(this, context); + } + + // ParenthesedInsert / ParenthesedUpdate / ParenthesedDelete need a StatementVisitor + if (statementVisitor != null && body instanceof Statement) { + return ((Statement) body).accept(statementVisitor, context); + } + + // a ClickHouse expression alias (WITH AS ): a + // parenthesized subquery stays on the select path, any other expression + // goes to the expression visitor + Expression expression = withItem.getExpression(); + if (expression != null) { + if (expression instanceof Select) { + return ((Select) expression).accept(this, context); + } + if (expressionVisitor != null) { + return expression.accept(expressionVisitor, context); + } + } + + // no statement visitor available: skip the body rather than fail + return null; } @Override - public T visit(Values aThis, S context) { + public T visit(Values values, S context) { + visitWithItems(values.getWithItemsList(), context); + expressionVisitor.visitExpressions(values.getExpressions(), context); + expressionVisitor.visitOrderBy(values.getOrderByElements(), context); + visitLimitOffsetFetch(values, context); return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SetOperation.java b/src/main/java/net/sf/jsqlparser/statement/select/SetOperation.java index 4438d75376..b08471bd82 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SetOperation.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SetOperation.java @@ -20,19 +20,35 @@ public String getModifier() { } public boolean isAll() { - return modifier != null && modifier.contains("ALL"); + return leadsWith("ALL"); } public void setAll(boolean all) { - this.modifier = "ALL"; + this.modifier = all ? "ALL" : ""; } public boolean isDistinct() { - return modifier != null && modifier.contains("DISTINCT"); + return leadsWith("DISTINCT"); } public void setDistinct(boolean distinct) { - this.modifier = "DISTINCT"; + this.modifier = distinct ? "DISTINCT" : ""; + } + + /** + * Whether the modifier opens with {@code keyword}, which the grammar records verbatim from the + * source and so may be written in any case. Anchored at the start and required to end on a word + * boundary, so a {@code CORRESPONDING BY NAME MATCHING(all)} column list cannot be mistaken for + * the ALL quantifier. + */ + private boolean leadsWith(String keyword) { + if (modifier == null) { + return false; + } + String trimmed = modifier.trim(); + return trimmed.regionMatches(true, 0, keyword, 0, keyword.length()) + && (trimmed.length() == keyword.length() + || !Character.isLetterOrDigit(trimmed.charAt(keyword.length()))); } private final SetOperationType type; diff --git a/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java b/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java index 51e4057ec8..1c688de50e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement.select; import java.util.List; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Function; @@ -17,6 +18,8 @@ @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) public class TableFunction extends Function implements FromItem { + private SampleClause sampleClause; + private String prefix = null; private Alias alias = null; private Pivot pivot = null; @@ -24,6 +27,8 @@ public class TableFunction extends Function implements FromItem { private Function function; private ParenthesedExpressionList rowsFromFunctions; private String withClause = null; + private boolean withOffset = false; + private Alias offsetAlias = null; public TableFunction(Function function) { this.function = function; @@ -121,6 +126,37 @@ public TableFunction setPrefix(String prefix) { return this; } + /** + * BigQuery's {@code UNNEST(array) [AS alias] WITH OFFSET [AS offsetAlias]}, where the + * {@code WITH OFFSET} clause follows the table alias and carries an alias of its own. + */ + public boolean isWithOffset() { + return withOffset; + } + + public void setWithOffset(boolean withOffset) { + this.withOffset = withOffset; + } + + public TableFunction withWithOffset(boolean withOffset) { + setWithOffset(withOffset); + return this; + } + + public Alias getOffsetAlias() { + return offsetAlias; + } + + public void setOffsetAlias(Alias offsetAlias) { + this.offsetAlias = offsetAlias; + this.withOffset = true; + } + + public TableFunction withOffsetAlias(Alias offsetAlias) { + setOffsetAlias(offsetAlias); + return this; + } + public String getWithClause() { return withClause; } @@ -186,22 +222,34 @@ public TableFunction withUnPivot(UnPivot unpivot) { @Override public SampleClause getSampleClause() { - return null; + return sampleClause; } @Override - public FromItem setSampleClause(SampleClause sampleClause) { - return null; + public TableFunction setSampleClause(SampleClause sampleClause) { + this.sampleClause = sampleClause; + return this; } public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { if (prefix != null) { builder.append(prefix).append(" "); } if (rowsFromFunctions != null) { - builder.append("ROWS FROM ").append(rowsFromFunctions); + builder.append("ROWS FROM ("); + for (int i = 0; i < rowsFromFunctions.size(); i++) { + if (i > 0) { + builder.append(", "); + } + expressionPrinter.accept(rowsFromFunctions.get(i)); + } + builder.append(")"); } else { - builder.append(function); + expressionPrinter.accept(function); } if (withClause != null) { @@ -211,6 +259,16 @@ public StringBuilder appendTo(StringBuilder builder) { if (alias != null) { builder.append(alias); } + + if (withOffset) { + builder.append(" WITH OFFSET"); + if (offsetAlias != null) { + builder.append(offsetAlias); + } + } + if (sampleClause != null) { + sampleClause.appendTo(builder); + } return builder; } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/TableStatement.java b/src/main/java/net/sf/jsqlparser/statement/select/TableStatement.java index d5c7dc152b..22d2d0cb44 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/TableStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/TableStatement.java @@ -30,24 +30,7 @@ public void setTable(Table table) { @Override public StringBuilder appendSelectBodyTo(StringBuilder builder) { - builder.append("TABLE ").append(table.getName()); - return builder; - } - - @SuppressWarnings({"PMD.CyclomaticComplexity"}) - @Override - public StringBuilder appendTo(StringBuilder builder) { - - appendSelectBodyTo(builder); - - builder.append(orderByToString(false, orderByElements)); - - if (limit != null) { - builder.append(limit); - } - if (offset != null) { - builder.append(offset); - } + builder.append("TABLE ").append(table); return builder; } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/UnPivotQuery.java b/src/main/java/net/sf/jsqlparser/statement/select/UnPivotQuery.java new file mode 100644 index 0000000000..799e6b6123 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/UnPivotQuery.java @@ -0,0 +1,115 @@ +/*- + * #%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.statement.select; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.schema.Column; + +/** + * DuckDB's simplified UNPIVOT statement: + * {@code UNPIVOT tbl ON columns [INTO NAME name VALUE values]}, the counterpart of + * {@link PivotQuery}. + * + * @see UNPIVOT + */ +public class UnPivotQuery extends Select { + private FromItem fromItem; + private ExpressionList onExpressions; + private String nameColumn; + private ExpressionList valueColumns; + + public FromItem getFromItem() { + return fromItem; + } + + public void setFromItem(FromItem fromItem) { + this.fromItem = fromItem; + } + + public UnPivotQuery withFromItem(FromItem fromItem) { + setFromItem(fromItem); + return this; + } + + public ExpressionList getOnExpressions() { + return onExpressions; + } + + public void setOnExpressions(ExpressionList onExpressions) { + this.onExpressions = onExpressions; + } + + public UnPivotQuery withOnExpressions(ExpressionList onExpressions) { + setOnExpressions(onExpressions); + return this; + } + + public String getNameColumn() { + return nameColumn; + } + + public void setNameColumn(String nameColumn) { + this.nameColumn = nameColumn; + } + + public UnPivotQuery withNameColumn(String nameColumn) { + setNameColumn(nameColumn); + return this; + } + + public ExpressionList getValueColumns() { + return valueColumns; + } + + public void setValueColumns(ExpressionList valueColumns) { + this.valueColumns = valueColumns; + } + + public UnPivotQuery withValueColumns(ExpressionList valueColumns) { + setValueColumns(valueColumns); + return this; + } + + @Override + public StringBuilder appendSelectBodyTo(StringBuilder builder) { + builder.append("UNPIVOT ").append(fromItem); + if (onExpressions != null) { + builder.append(" ON ").append(onExpressions); + } + if (nameColumn != null) { + builder.append(" INTO NAME ").append(nameColumn); + if (valueColumns != null) { + builder.append(" VALUE ").append(valueColumns); + } + } + return builder; + } + + @Override + public T accept(SelectVisitor selectVisitor, S context) { + return selectVisitor.visit(this, context); + } + + @Override + public T accept(FromItemVisitor fromItemVisitor, S context) { + return fromItemVisitor.visit(this, context); + } + + @Override + public SampleClause getSampleClause() { + return null; + } + + @Override + public FromItem setSampleClause(SampleClause sampleClause) { + return null; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/WithCycleClause.java b/src/main/java/net/sf/jsqlparser/statement/select/WithCycleClause.java new file mode 100644 index 0000000000..5a0cc5155b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/WithCycleClause.java @@ -0,0 +1,129 @@ +/*- + * #%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.statement.select; + +import java.io.Serializable; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.schema.Column; + +/** Cycle detection metadata for a recursive common table expression. */ +public class WithCycleClause implements Serializable { + private ExpressionList cycleColumns; + private String markColumnName; + private Expression markValue; + private Expression markDefault; + private String pathColumnName; + + public ExpressionList getCycleColumns() { + return cycleColumns; + } + + public void setCycleColumns(ExpressionList cycleColumns) { + this.cycleColumns = cycleColumns; + } + + public WithCycleClause withCycleColumns(ExpressionList cycleColumns) { + setCycleColumns(cycleColumns); + return this; + } + + public String getMarkColumnName() { + return markColumnName; + } + + public void setMarkColumnName(String markColumnName) { + this.markColumnName = markColumnName; + } + + public WithCycleClause withMarkColumnName(String markColumnName) { + setMarkColumnName(markColumnName); + return this; + } + + public Expression getMarkValue() { + return markValue; + } + + public void setMarkValue(Expression markValue) { + this.markValue = markValue; + } + + public WithCycleClause withMarkValue(Expression markValue) { + setMarkValue(markValue); + return this; + } + + public Expression getMarkDefault() { + return markDefault; + } + + public void setMarkDefault(Expression markDefault) { + this.markDefault = markDefault; + } + + public WithCycleClause withMarkDefault(Expression markDefault) { + setMarkDefault(markDefault); + return this; + } + + public String getPathColumnName() { + return pathColumnName; + } + + public void setPathColumnName(String pathColumnName) { + this.pathColumnName = pathColumnName; + } + + public WithCycleClause withPathColumnName(String pathColumnName) { + setPathColumnName(pathColumnName); + return this; + } + + public void accept(ExpressionVisitor visitor, S context) { + if (cycleColumns != null) { + cycleColumns.forEach(column -> column.accept(visitor, context)); + } + if (markValue != null) { + markValue.accept(visitor, context); + } + if (markDefault != null) { + markDefault.accept(visitor, context); + } + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append("CYCLE "); + if (cycleColumns != null) { + for (int i = 0; i < cycleColumns.size(); i++) { + if (i > 0) { + builder.append(", "); + } + expressionPrinter.accept(cycleColumns.get(i)); + } + } + builder.append(" SET ").append(markColumnName); + if (markValue != null) { + builder.append(" TO "); + expressionPrinter.accept(markValue); + builder.append(" DEFAULT "); + expressionPrinter.accept(markDefault); + } + return builder.append(" USING ").append(pathColumnName); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, expression -> builder.append(expression)).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/WithFill.java b/src/main/java/net/sf/jsqlparser/statement/select/WithFill.java new file mode 100644 index 0000000000..9da5672512 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/WithFill.java @@ -0,0 +1,102 @@ +/*- + * #%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.statement.select; + +import java.io.Serializable; + +import net.sf.jsqlparser.expression.Expression; + +/** + * Models the ClickHouse {@code WITH FILL} modifier of an {@link OrderByElement}, which fills gaps + * in the sorted sequence of the order by expression (see + * ClickHouse: + * ORDER BY). + *

+ * The modifier takes optional {@code FROM}, {@code TO}, {@code STEP} and {@code STALENESS} bounds, + * where the step may be an {@code INTERVAL} literal. Its presence on an {@link OrderByElement} + * marks the element as filled. + */ +public class WithFill implements Serializable { + + private Expression from; + private Expression to; + private Expression step; + private Expression staleness; + + public Expression getFrom() { + return from; + } + + public void setFrom(Expression from) { + this.from = from; + } + + public WithFill withFrom(Expression from) { + setFrom(from); + return this; + } + + public Expression getTo() { + return to; + } + + public void setTo(Expression to) { + this.to = to; + } + + public WithFill withTo(Expression to) { + setTo(to); + return this; + } + + public Expression getStep() { + return step; + } + + public void setStep(Expression step) { + this.step = step; + } + + public WithFill withStep(Expression step) { + setStep(step); + return this; + } + + public Expression getStaleness() { + return staleness; + } + + public void setStaleness(Expression staleness) { + this.staleness = staleness; + } + + public WithFill withStaleness(Expression staleness) { + setStaleness(staleness); + return this; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder("WITH FILL"); + if (from != null) { + b.append(" FROM ").append(from); + } + if (to != null) { + b.append(" TO ").append(to); + } + if (step != null) { + b.append(" STEP ").append(step); + } + if (staleness != null) { + b.append(" STALENESS ").append(staleness); + } + return b.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java b/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java index 01605c8a91..90cd9c0c5c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java @@ -9,6 +9,10 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; @@ -27,10 +31,13 @@ public class WithItem implements Serializable { private Alias alias; private List> withItemList; private WithFunctionDeclaration withFunctionDeclaration; + private Expression expression; private WithSearchClause searchClause; + private WithCycleClause cycleClause; private boolean recursive = false; private boolean usingNot = false; private boolean materialized = false; + private ExpressionList usingKeyExpressions; public WithItem(K statement, Alias alias) { this.statement = statement; @@ -41,6 +48,25 @@ public WithItem() { this(null, (Alias) null); } + /** + * The aliased expression of a WITH item like ClickHouse's + * {@code WITH AS }, where the item does not hold a statement. + * + * @return the expression of this WITH item, or null for statement-based (CTE) items + */ + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public WithItem withExpression(Expression expression) { + this.setExpression(expression); + return this; + } + public K getParenthesedStatement() { return statement; } @@ -149,11 +175,60 @@ public WithItem withSearchClause(WithSearchClause searchClause) { return this; } + public WithCycleClause getCycleClause() { + return cycleClause; + } + + public void setCycleClause(WithCycleClause cycleClause) { + this.cycleClause = cycleClause; + } + + public WithItem withCycleClause(WithCycleClause cycleClause) { + setCycleClause(cycleClause); + return this; + } + + /** + * DuckDB 2.0 aggregates a recursive CTE by key: + * {@code WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS (...)}. + */ + public ExpressionList getUsingKeyExpressions() { + return usingKeyExpressions; + } + + public void setUsingKeyExpressions(ExpressionList usingKeyExpressions) { + this.usingKeyExpressions = usingKeyExpressions; + } + + public WithItem withUsingKeyExpressions(ExpressionList usingKeyExpressions) { + setUsingKeyExpressions(usingKeyExpressions); + return this; + } + + public StringBuilder appendRecursiveClausesTo(StringBuilder builder, + Consumer expressionPrinter) { + if (searchClause != null) { + builder.append(" "); + searchClause.appendTo(builder, expressionPrinter); + } + if (cycleClause != null) { + builder.append(" "); + cycleClause.appendTo(builder, expressionPrinter); + } + return builder; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); if (withFunctionDeclaration != null) { builder.append(withFunctionDeclaration); + } else if (expression != null) { + builder.append(expression); + if (alias != null) { + builder.append(" AS ").append(alias.getName()); + } + appendRecursiveClausesTo(builder, expr -> builder.append(expr)); } else { builder.append(recursive ? "RECURSIVE " : ""); if (alias != null) { @@ -167,6 +242,9 @@ public String toString() { } builder.append(")"); } + if (usingKeyExpressions != null) { + builder.append(" USING KEY (").append(usingKeyExpressions).append(")"); + } builder.append(" AS "); if (materialized) { builder.append(usingNot @@ -174,9 +252,7 @@ public String toString() { : "MATERIALIZED "); } builder.append(statement); - if (searchClause != null) { - builder.append(" ").append(searchClause); - } + appendRecursiveClausesTo(builder, expression -> builder.append(expression)); } return builder.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/WithSearchClause.java b/src/main/java/net/sf/jsqlparser/statement/select/WithSearchClause.java index 84de6e2233..75f7a1949e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/WithSearchClause.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/WithSearchClause.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + import java.io.Serializable; import java.util.Collection; @@ -86,15 +89,23 @@ public WithSearchClause withSequenceColumnName(String sequenceColumnName) { return this; } + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { + builder.append("SEARCH ").append(searchOrder).append(" FIRST BY "); + if (searchColumns != null) { + for (int i = 0; i < searchColumns.size(); i++) { + if (i > 0) { + builder.append(", "); + } + expressionPrinter.accept(searchColumns.get(i)); + } + } + return builder.append(" SET ").append(sequenceColumnName); + } + @Override public String toString() { - return new StringBuilder() - .append("SEARCH ") - .append(searchOrder) - .append(" FIRST BY ") - .append(Select.getStringList(searchColumns)) - .append(" SET ") - .append(sequenceColumnName) - .toString(); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, expression -> builder.append(expression)).toString(); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/show/ShowCreateStatement.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowCreateStatement.java new file mode 100644 index 0000000000..d124fc2301 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowCreateStatement.java @@ -0,0 +1,192 @@ +/*- + * #%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.statement.show; + +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.user.UserAccount; + +/** + * A MySQL {@code SHOW CREATE} statement, which returns the definition of one object. + * + *

    + *
  • {@code TABLE}, {@code VIEW}, {@code PROCEDURE}, {@code FUNCTION}, {@code TRIGGER} and + * {@code EVENT} name the object with {@link #getName()}.
  • + *
  • {@code DATABASE} (also spelled {@code SCHEMA}) uses {@link #getDatabaseName()} and may carry + * {@code IF NOT EXISTS}.
  • + *
  • {@code USER} uses {@link #getAccount()}, which is {@code CURRENT_USER} for the session + * account.
  • + *
+ * + * @see MySQL SHOW CREATE + * TABLE + */ +public class ShowCreateStatement extends ASTNodeAccessImpl implements Statement { + + public enum ObjectType { + TABLE, VIEW, PROCEDURE, FUNCTION, TRIGGER, EVENT, DATABASE, USER; + + /** Whether objects of this type are named by {@link ShowCreateStatement#getName()}. */ + public boolean isNamedByTable() { + return this != DATABASE && this != USER; + } + } + + private ObjectType objectType; + private boolean schemaKeyword; + private boolean ifNotExists; + private Table name; + private String databaseName; + private UserAccount account; + private boolean currentUserParentheses; + + public ShowCreateStatement() { + // empty constructor + } + + public ShowCreateStatement(ObjectType objectType, Table name) { + this.objectType = objectType; + this.name = name; + } + + public ObjectType getObjectType() { + return objectType; + } + + public void setObjectType(ObjectType objectType) { + this.objectType = objectType; + } + + /** Whether {@code DATABASE} was spelled {@code SCHEMA}. */ + public boolean isSchemaKeyword() { + return schemaKeyword; + } + + public void setSchemaKeyword(boolean schemaKeyword) { + this.schemaKeyword = schemaKeyword; + } + + /** Whether {@code SHOW CREATE DATABASE IF NOT EXISTS} was written. */ + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + /** The table, view, routine, trigger or event name; null for DATABASE and USER. */ + public Table getName() { + return name; + } + + public void setName(Table name) { + this.name = name; + } + + /** The database name of {@code SHOW CREATE DATABASE}; null otherwise. */ + public String getDatabaseName() { + return databaseName; + } + + public void setDatabaseName(String databaseName) { + this.databaseName = databaseName; + } + + /** The account of {@code SHOW CREATE USER}; null otherwise. */ + public UserAccount getAccount() { + return account; + } + + public void setAccount(UserAccount account) { + this.account = account; + } + + /** Whether the account was written as {@code CURRENT_USER()}. */ + public boolean isCurrentUserParentheses() { + return currentUserParentheses; + } + + public void setCurrentUserParentheses(boolean currentUserParentheses) { + this.currentUserParentheses = currentUserParentheses; + } + + /** Whether the account is {@code CURRENT_USER} or {@code CURRENT_USER()}. */ + public boolean isCurrentUser() { + if (account == null || account.getHost() != null || account.getUser() == null) { + return false; + } + StringValue user = account.getUser(); + return "".equals(user.getQuoteStr()) && "CURRENT_USER".equalsIgnoreCase(user.getValue()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("SHOW CREATE "); + if (objectType == ObjectType.DATABASE) { + builder.append(schemaKeyword ? "SCHEMA" : "DATABASE"); + if (ifNotExists) { + builder.append(" IF NOT EXISTS"); + } + builder.append(' ').append(databaseName); + } else if (objectType == ObjectType.USER) { + builder.append("USER ").append(account); + if (currentUserParentheses) { + builder.append("()"); + } + } else { + builder.append(objectType).append(' ').append(name); + } + return builder.toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + public ShowCreateStatement withObjectType(ObjectType objectType) { + this.setObjectType(objectType); + return this; + } + + public ShowCreateStatement withSchemaKeyword(boolean schemaKeyword) { + this.setSchemaKeyword(schemaKeyword); + return this; + } + + public ShowCreateStatement withIfNotExists(boolean ifNotExists) { + this.setIfNotExists(ifNotExists); + return this; + } + + public ShowCreateStatement withName(Table name) { + this.setName(name); + return this; + } + + public ShowCreateStatement withDatabaseName(String databaseName) { + this.setDatabaseName(databaseName); + return this; + } + + public ShowCreateStatement withAccount(UserAccount account) { + this.setAccount(account); + return this; + } + + public ShowCreateStatement withCurrentUserParentheses(boolean currentUserParentheses) { + this.setCurrentUserParentheses(currentUserParentheses); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/show/ShowIndexStatement.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowIndexStatement.java index d6b4bf72d8..3c9b10072d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/show/ShowIndexStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowIndexStatement.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement.show; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; @@ -16,7 +17,7 @@ * @author Jayant Kumar Yadav */ -public class ShowIndexStatement implements Statement { +public class ShowIndexStatement extends ASTNodeAccessImpl implements Statement { private String tableName; diff --git a/src/main/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatement.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatement.java new file mode 100644 index 0000000000..a5126cf7ca --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatement.java @@ -0,0 +1,103 @@ +/*- + * #%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.statement.show; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; + +/** + * A MySQL {@code SHOW TABLE STATUS [{FROM | IN} db] [LIKE 'pattern' | WHERE expr]} statement. + * + * @see MySQL SHOW TABLE + * STATUS + */ +public class ShowTableStatusStatement extends ASTNodeAccessImpl implements Statement { + + private SelectionMode selectionMode = SelectionMode.FROM; + private String dbName; + private Expression likeExpression; + private Expression whereCondition; + + /** The keyword before the database name, {@code FROM} unless {@code IN} was written. */ + public SelectionMode getSelectionMode() { + return selectionMode; + } + + public void setSelectionMode(SelectionMode selectionMode) { + this.selectionMode = selectionMode; + } + + public String getDbName() { + return dbName; + } + + public void setDbName(String dbName) { + this.dbName = dbName; + } + + public Expression getLikeExpression() { + return likeExpression; + } + + public void setLikeExpression(Expression likeExpression) { + this.likeExpression = likeExpression; + } + + public Expression getWhereCondition() { + return whereCondition; + } + + public void setWhereCondition(Expression whereCondition) { + this.whereCondition = whereCondition; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("SHOW TABLE STATUS"); + if (dbName != null) { + builder.append(' ').append(selectionMode == null ? SelectionMode.FROM : selectionMode) + .append(' ').append(dbName); + } + if (likeExpression != null) { + builder.append(" LIKE ").append(likeExpression); + } else if (whereCondition != null) { + builder.append(" WHERE ").append(whereCondition); + } + return builder.toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + public ShowTableStatusStatement withSelectionMode(SelectionMode selectionMode) { + this.setSelectionMode(selectionMode); + return this; + } + + public ShowTableStatusStatement withDbName(String dbName) { + this.setDbName(dbName); + return this; + } + + public ShowTableStatusStatement withLikeExpression(Expression likeExpression) { + this.setLikeExpression(likeExpression); + return this; + } + + public ShowTableStatusStatement withWhereCondition(Expression whereCondition) { + this.setWhereCondition(whereCondition); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/show/ShowTablesStatement.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowTablesStatement.java index d6016d7fb9..7dbb97b479 100644 --- a/src/main/java/net/sf/jsqlparser/statement/show/ShowTablesStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowTablesStatement.java @@ -11,6 +11,7 @@ import java.util.Locale; import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; @@ -21,7 +22,7 @@ * * @see MySQL show tables */ -public class ShowTablesStatement implements Statement { +public class ShowTablesStatement extends ASTNodeAccessImpl implements Statement { private EnumSet modifiers; private SelectionMode selectionMode; diff --git a/src/main/java/net/sf/jsqlparser/statement/truncate/Truncate.java b/src/main/java/net/sf/jsqlparser/statement/truncate/Truncate.java index 58442d38ee..a046c7fce1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/truncate/Truncate.java +++ b/src/main/java/net/sf/jsqlparser/statement/truncate/Truncate.java @@ -9,113 +9,228 @@ */ package net.sf.jsqlparser.statement.truncate; -import static java.util.stream.Collectors.joining; - +import java.util.AbstractList; +import java.util.ArrayList; import java.util.List; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.schema.TableReference; +import net.sf.jsqlparser.statement.CascadeBehavior; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; -public class Truncate implements Statement { +public class Truncate extends ASTNodeAccessImpl implements Statement { + public enum IdentityOption { + RESTART, CONTINUE + } + + public enum StorageOption { + DROP, DROP_ALL, REUSE + } + + public enum MaterializedViewLogOption { + PRESERVE, PURGE + } - boolean cascade; // to support TRUNCATE TABLE ... CASCADE - boolean tableToken; // to support TRUNCATE without TABLE - boolean only; // to support TRUNCATE with ONLY - private Table table; - private List
tables; + private boolean tableToken; + private boolean only; + private final List targets = new ArrayList<>(); + private IdentityOption identityOption; + private CascadeBehavior dropBehavior; + private StorageOption storageOption; + private MaterializedViewLogOption materializedViewLogOption; @Override public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); } - public Table getTable() { - return table; + public List getTargets() { + return targets; } - public List
getTables() { - return tables; + public void setTargets(List references) { + List replacement = new ArrayList<>(references); + targets.clear(); + targets.addAll(replacement); + } + + /** Preserves the legacy parser's singular accessor for the last target. */ + public Table getTable() { + return targets.isEmpty() ? null : targets.get(targets.size() - 1).getTable(); } public void setTable(Table table) { - this.table = table; + if (targets.isEmpty()) { + TableReference target = new TableReference(table); + target.setOnly(only); + targets.add(target); + } else { + targets.get(targets.size() - 1).setTable(table); + } + } + + /** Mutable table projection; replacing a table preserves its target's inheritance scope. */ + public List
getTables() { + return new AbstractList
() { + @Override + public Table get(int index) { + return targets.get(index).getTable(); + } + + @Override + public int size() { + return targets.size(); + } + + @Override + public Table set(int index, Table table) { + Table previous = get(index); + targets.get(index).setTable(table); + return previous; + } + + @Override + public void add(int index, Table table) { + targets.add(index, new TableReference(table)); + } + + @Override + public Table remove(int index) { + return targets.remove(index).getTable(); + } + }; } public void setTables(List
tables) { - this.tables = tables; + List
replacement = tables == null ? new ArrayList<>() : new ArrayList<>(tables); + boolean firstOnly = isOnly(); + targets.clear(); + replacement.forEach(table -> targets.add(new TableReference(table))); + setOnly(firstOnly); } - public boolean getCascade() { - return cascade; + public IdentityOption getIdentityOption() { + return identityOption; } - public void setCascade(boolean c) { - cascade = c; + public void setIdentityOption(IdentityOption option) { + identityOption = option; } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("TRUNCATE"); - if (tableToken) { - sb.append(" TABLE"); - } - if (only) { - sb.append(" ONLY"); - } - sb.append(" "); - if (tables != null && !tables.isEmpty()) { - sb.append(tables.stream() - .map(Table::toString) - .collect(joining(", "))); - } else { - sb.append(table); - } - if (cascade) { - sb.append(" CASCADE"); - } - return sb.toString(); + public StorageOption getStorageOption() { + return storageOption; + } + + public void setStorageOption(StorageOption storageOption) { + this.storageOption = storageOption; + } + + public Truncate withStorageOption(StorageOption storageOption) { + setStorageOption(storageOption); + return this; + } + + public MaterializedViewLogOption getMaterializedViewLogOption() { + return materializedViewLogOption; + } + + public void setMaterializedViewLogOption(MaterializedViewLogOption option) { + materializedViewLogOption = option; + } + + public Truncate withMaterializedViewLogOption(MaterializedViewLogOption option) { + setMaterializedViewLogOption(option); + return this; + } + + public CascadeBehavior getDropBehavior() { + return dropBehavior; + } + + public void setDropBehavior(CascadeBehavior behavior) { + dropBehavior = behavior; + } + + public boolean getCascade() { + return dropBehavior == CascadeBehavior.CASCADE; + } + + public void setCascade(boolean cascade) { + dropBehavior = cascade ? CascadeBehavior.CASCADE : null; } public boolean isTableToken() { return tableToken; } - public void setTableToken(boolean hasTable) { - this.tableToken = hasTable; + public void setTableToken(boolean tableToken) { + this.tableToken = tableToken; } + /** Legacy ONLY accessor describes the first target. */ public boolean isOnly() { - return only; + return targets.isEmpty() ? only : targets.get(0).isOnly(); } public void setOnly(boolean only) { this.only = only; + if (!targets.isEmpty()) { + targets.get(0).setOnly(only); + } + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(tableToken ? "TRUNCATE TABLE " : "TRUNCATE "); + for (int i = 0; i < targets.size(); i++) { + if (i > 0) { + builder.append(", "); + } + targets.get(i).appendTo(builder); + } + if (materializedViewLogOption != null) { + builder.append(' ').append(materializedViewLogOption).append(" MATERIALIZED VIEW LOG"); + } + if (storageOption != null) { + builder.append(' ').append(storageOption == StorageOption.DROP_ALL ? "DROP ALL" + : storageOption.name()).append(" STORAGE"); + } + if (identityOption != null) { + builder.append(' ').append(identityOption).append(" IDENTITY"); + } + if (dropBehavior != null) { + builder.append(' ').append(dropBehavior); + } + return builder; } - public Truncate withTableToken(boolean hasTableToken) { - this.setTableToken(hasTableToken); + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + public Truncate withTableToken(boolean value) { + setTableToken(value); return this; } - public Truncate withTable(Table table) { - this.setTable(table); + public Truncate withTable(Table value) { + setTable(value); return this; } - public Truncate withTables(List
tables) { - this.setTables(tables); + public Truncate withTables(List
value) { + setTables(value); return this; } - public Truncate withCascade(boolean cascade) { - this.setCascade(cascade); + public Truncate withCascade(boolean value) { + setCascade(value); return this; } - public Truncate withOnly(boolean only) { - this.setOnly(only); + public Truncate withOnly(boolean value) { + setOnly(value); return this; } } - diff --git a/src/main/java/net/sf/jsqlparser/statement/update/Update.java b/src/main/java/net/sf/jsqlparser/statement/update/Update.java index b09854fe69..6351832276 100644 --- a/src/main/java/net/sf/jsqlparser/statement/update/Update.java +++ b/src/main/java/net/sf/jsqlparser/statement/update/Update.java @@ -12,8 +12,10 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.expression.PreferringClause; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.ForPortionClause; import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.ReturningClause; import net.sf.jsqlparser.statement.Statement; @@ -21,6 +23,7 @@ import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.Limit; +import net.sf.jsqlparser.statement.select.OptionClause; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; @@ -35,19 +38,22 @@ import java.util.Optional; @SuppressWarnings({"PMD.CyclomaticComplexity"}) -public class Update implements Statement { +public class Update extends ASTNodeAccessImpl implements Statement { private List> withItemsList; private Table table; + private ForPortionClause forPortionClause; private Expression where; private PreferringClause preferringClause; private List updateSets; private FromItem fromItem; + private boolean fromBeforeSet; private List joins; private List startJoins; private OracleHint oracleHint = null; private List orderByElements; private Limit limit; + private OptionClause option; private ReturningClause returningClause; private UpdateModifierPriority modifierPriority; private boolean modifierIgnore; @@ -111,6 +117,19 @@ public Update addWithItemsList(Collection> withItemsList) return this.withWithItemsList(collection); } + public ForPortionClause getForPortionClause() { + return forPortionClause; + } + + public void setForPortionClause(ForPortionClause forPortionClause) { + this.forPortionClause = forPortionClause; + } + + public Update withForPortionClause(ForPortionClause forPortionClause) { + setForPortionClause(forPortionClause); + return this; + } + public Table getTable() { return table; } @@ -188,6 +207,50 @@ public void setFromItem(FromItem fromItem) { this.fromItem = fromItem; } + /** Whether the FROM clause precedes SET, as in Teradata joined updates. */ + public boolean isFromBeforeSet() { + return fromBeforeSet; + } + + public void setFromBeforeSet(boolean fromBeforeSet) { + this.fromBeforeSet = fromBeforeSet; + } + + public Update withFromBeforeSet(boolean fromBeforeSet) { + setFromBeforeSet(fromBeforeSet); + return this; + } + + /** Whether the Teradata target names an alias declared in this UPDATE's FROM clause. */ + public boolean isTargetTableAlias() { + if (!fromBeforeSet || table == null) { + return false; + } + if (matchesTargetAlias(fromItem)) { + return true; + } + return joins != null + && joins.stream().anyMatch(join -> matchesTargetAlias(join.getRightItem())); + } + + private boolean matchesTargetAlias(FromItem source) { + return source != null && source.getAlias() != null + && table.getFullyQualifiedName().equalsIgnoreCase(source.getAlias().getName()); + } + + /** Shared FROM rendering for both supported clause positions. */ + public StringBuilder appendFromTo(StringBuilder builder) { + if (fromItem != null) { + builder.append(" FROM ").append(fromItem); + if (joins != null) { + for (Join join : joins) { + builder.append(join.isSimple() ? ", " : " ").append(join); + } + } + } + return builder; + } + public List getJoins() { return joins; } @@ -255,6 +318,19 @@ public Limit getLimit() { return limit; } + public OptionClause getOption() { + return option; + } + + public Update setOption(OptionClause option) { + this.option = option; + return this; + } + + public Update withOption(OptionClause option) { + return setOption(option); + } + public void setLimit(Limit limit) { this.limit = limit; } @@ -312,6 +388,9 @@ public String toString() { b.append("IGNORE "); } b.append(table); + if (forPortionClause != null) { + b.append(' ').append(forPortionClause); + } if (startJoins != null) { for (Join join : startJoins) { if (join.isSimple()) { @@ -322,6 +401,9 @@ public String toString() { } } + if (fromBeforeSet) { + appendFromTo(b); + } b.append(" SET "); UpdateSet.appendUpdateSetsTo(b, updateSets); @@ -329,17 +411,8 @@ public String toString() { outputClause.appendTo(b); } - if (fromItem != null) { - b.append(" FROM ").append(fromItem); - if (joins != null) { - for (Join join : joins) { - if (join.isSimple()) { - b.append(", ").append(join); - } else { - b.append(" ").append(join); - } - } - } + if (!fromBeforeSet) { + appendFromTo(b); } if (where != null) { @@ -356,6 +429,10 @@ public String toString() { b.append(limit); } + if (option != null) { + b.append(option); + } + if (returningClause != null) { returningClause.appendTo(b); } diff --git a/src/main/java/net/sf/jsqlparser/statement/upsert/Upsert.java b/src/main/java/net/sf/jsqlparser/statement/upsert/Upsert.java index c13397569d..f4a0492363 100644 --- a/src/main/java/net/sf/jsqlparser/statement/upsert/Upsert.java +++ b/src/main/java/net/sf/jsqlparser/statement/upsert/Upsert.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement.upsert; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; @@ -27,7 +28,7 @@ import java.util.List; import java.util.Optional; -public class Upsert implements Statement { +public class Upsert extends ASTNodeAccessImpl implements Statement { private Table table; private ExpressionList columns; diff --git a/src/main/java/net/sf/jsqlparser/util/AddAliasesVisitor.java b/src/main/java/net/sf/jsqlparser/util/AddAliasesVisitor.java index e3b1745082..fa3649a658 100644 --- a/src/main/java/net/sf/jsqlparser/util/AddAliasesVisitor.java +++ b/src/main/java/net/sf/jsqlparser/util/AddAliasesVisitor.java @@ -9,8 +9,8 @@ */ package net.sf.jsqlparser.util; -import java.util.LinkedList; -import java.util.List; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.Locale; import net.sf.jsqlparser.expression.Alias; @@ -37,7 +37,7 @@ public class AddAliasesVisitor implements SelectVisitor, SelectItemVisitor { private static final String NOT_SUPPORTED_YET = "Not supported yet."; - private final List aliases = new LinkedList(); + private final Set aliases = new LinkedHashSet(); private boolean firstRun = true; private int counter = 0; private String prefix = "A"; diff --git a/src/main/java/net/sf/jsqlparser/util/ConnectExpressionsVisitor.java b/src/main/java/net/sf/jsqlparser/util/ConnectExpressionsVisitor.java index ca642b7a11..7c97f57972 100644 --- a/src/main/java/net/sf/jsqlparser/util/ConnectExpressionsVisitor.java +++ b/src/main/java/net/sf/jsqlparser/util/ConnectExpressionsVisitor.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -70,15 +71,16 @@ public T visit(PlainSelect plainSelect, S context) { } if (itemsExpr.size() > 1) { + Iterator> it = itemsExpr.iterator(); BinaryExpression binExpr = createBinaryExpression(); - binExpr.setLeftExpression(itemsExpr.get(0).getExpression()); - for (int i = 1; i < itemsExpr.size() - 1; i++) { - binExpr.setRightExpression(itemsExpr.get(i).getExpression()); + binExpr.setLeftExpression(it.next().getExpression()); + binExpr.setRightExpression(it.next().getExpression()); + while (it.hasNext()) { BinaryExpression binExpr2 = createBinaryExpression(); binExpr2.setLeftExpression(binExpr); + binExpr2.setRightExpression(it.next().getExpression()); binExpr = binExpr2; } - binExpr.setRightExpression(itemsExpr.get(itemsExpr.size() - 1).getExpression()); SelectItem sei = new SelectItem<>(); sei.setExpression(binExpr); diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java new file mode 100644 index 0000000000..a6d1f05635 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -0,0 +1,293 @@ +/*- + * #%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.util; + +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.KeyElement; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.LikeClause; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterOperation; +import net.sf.jsqlparser.statement.alter.AlterExpressionPartition; +import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; +import net.sf.jsqlparser.statement.alter.AlterForeignDataOptions; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.DefaultConstraint; +import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.KeyColumnSource; +import net.sf.jsqlparser.statement.create.table.IndexOptionSource; +import net.sf.jsqlparser.statement.create.table.NotNullConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.TableElement; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; +import net.sf.jsqlparser.statement.create.table.PartitionBound; + +/** Traverses structured table definitions without interpreting legacy raw column options. */ +public final class TableDefinitionTraversal { + private TableDefinitionTraversal() {} + + public static void visit(CreateIndex createIndex, Consumer expressions, + Consumer
tables) { + accept(createIndex.getTable(), tables); + if (createIndex.getIndex() != null) { + visit(createIndex.getIndex(), expressions, tables); + } else { + visitOptions(createIndex.getStorageParameters(), expressions); + } + accept(createIndex.getWhere(), expressions); + } + + /** + * Visits expressions and tables contributing to the rendered ALTER action. Inactive metadata is + * retained by the AST's getters but is not visited. + */ + public static void visit(AlterExpression action, Consumer expressions, + Consumer
tables) { + if (visitSpecialBody(action, expressions, tables)) { + return; + } + if (action.getOperation() == AlterOperation.RENAME_TABLE + && action.getOldIndex() == null + && !(action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionDrop)) { + accept(action.getNewTable(), tables); + } + action.getColumnDefaults() + .forEach(column -> accept(column.getDefaultExpression(), expressions)); + if (action.hasActiveColumnDefinitions()) { + action.getColDataTypeList().forEach(column -> visit(column, expressions, tables)); + } + TableElement definition = action.getActiveTableDefinition(); + if (action.hasActiveConstraintHeader() && definition instanceof KeyColumnSource) { + visitKeys((KeyColumnSource) definition, expressions); + } else if (definition != null) { + visit(definition, expressions, tables); + } + } + + private static boolean visitSpecialBody(AlterExpression action, + Consumer expressions, + Consumer
tables) { + if (action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) { + ((net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) action).getOrderByElements() + .forEach(element -> accept(element.getExpression(), expressions)); + } else if (action instanceof net.sf.jsqlparser.statement.alter.RelationAlterAction) { + net.sf.jsqlparser.statement.alter.RelationAlterAction relation = + (net.sf.jsqlparser.statement.alter.RelationAlterAction) action; + relation.visitExpressions(expressions); + relation.visitTables(tables); + } else if (action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionTableOption) { + visit(((net.sf.jsqlparser.statement.alter.AlterExpressionTableOption) action) + .getStructuredTableOption(), expressions, tables); + } else if (action instanceof AlterForeignDataOptions) { + ForeignDataOption.visitExpressions(((AlterForeignDataOptions) action).getOptions(), + expressions); + } else if (action instanceof AlterExpressionPartition) { + visitPartitionAction((AlterExpressionPartition) action, expressions, tables); + } else if (action instanceof AlterExpressionPrimaryKey) { + AlterExpressionPrimaryKey primaryKey = (AlterExpressionPrimaryKey) action; + visit(primaryKey.getConstraint(), expressions, tables); + if (primaryKey.isUsingHash()) { + accept(primaryKey.getBucketCount(), expressions); + } + } else { + return action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionCharset + || action instanceof net.sf.jsqlparser.statement.alter.AlterConstraintAttributes; + } + return true; + } + + private static void visitPartitionAction(AlterExpressionPartition partition, + Consumer expressions, Consumer
tables) { + switch (partition.getOperation()) { + case ATTACH_PARTITION: + accept(partition.getPartitionTable(), tables); + visit(partition.getPartitionBound(), expressions); + break; + case DETACH_PARTITION: + accept(partition.getPartitionTable(), tables); + break; + case EXCHANGE_PARTITION: + accept(partition.getExchangeTable(), tables); + break; + case PARTITION_BY: + visit(partition.getPartitioning(), expressions); + break; + default: + break; + } + } + + public static void visit(CreateTable table, Consumer expressions, + Consumer
tables) { + if (table.getTableElements() != null) { + table.getTableElements().forEach(element -> visit(element, expressions, tables)); + } else { + if (table.getColumnDefinitions() != null) { + table.getColumnDefinitions().forEach(column -> visit(column, expressions, tables)); + } + if (table.getTableConstraints() != null) { + table.getTableConstraints() + .forEach(constraint -> visit(constraint, expressions, tables)); + } + if (table.getIndexes() != null) { + table.getIndexes().forEach(index -> visit(index, expressions, tables)); + } + } + if (table.getForeignTableOptions() != null) { + ForeignDataOption.visitExpressions(table.getForeignTableOptions().getOptions(), + expressions); + } + if (table.getTableOptions() != null) { + table.getTableOptions().forEach(option -> visit(option, expressions, tables)); + } + if (table.getInherits() != null) { + table.getInherits().forEach(parent -> accept(parent, tables)); + } + if (table.getExecute() != null && table.getExecute().getExprList() != null) { + accept(table.getExecute().getExprList(), expressions); + } + accept(table.getTrailingLikeTable(), tables); + accept(table.getPartitionOf(), tables); + visit(table.getPartitioning(), expressions); + visit(table.getPartitionBound(), expressions); + } + + /** Shared CREATE/ALTER table option traversal, including COMMENT literals. */ + public static void visit(net.sf.jsqlparser.statement.create.table.TableOption option, + Consumer expressions, Consumer
tables) { + if (option != null) { + visitOptions(option.getStorageParameters(), expressions); + accept(option.getComment(), expressions); + if (option.getUnionTables() != null) { + option.getUnionTables().forEach(source -> accept(source, tables)); + } + } + } + + /** Visits the active partition key and any subpartition key. Raw bounds remain opaque. */ + public static void visit(TablePartitioning partitioning, Consumer expressions) { + if (partitioning == null) { + return; + } + if (partitioning.getKeyColumns() != null) { + partitioning.getKeyColumns().forEach(key -> { + accept(key.getExpression(), expressions); + visitOptions(key.getOperatorClassParameters(), expressions); + }); + } else if (partitioning.getExpression() != null) { + accept(partitioning.getExpression(), expressions); + } else if (partitioning.getExpressionList() != null) { + accept(partitioning.getExpressionList(), expressions); + } else { + accept(partitioning.getColumns(), expressions); + } + visit(partitioning.getSubPartitioning(), expressions); + } + + /** Visits expressions belonging to the selected PostgreSQL bound type. */ + public static void visit(PartitionBound bound, Consumer expressions) { + if (bound != null) { + bound.visitExpressions(expressions); + } + } + + public static void visit(TableElement element, Consumer expressions, + Consumer
tables) { + if (element instanceof LikeClause) { + accept(((LikeClause) element).getTable(), tables); + } else if (element instanceof ColumnDefinition) { + visitColumn((ColumnDefinition) element, expressions, tables); + } else if (element instanceof NamedConstraint) { + visitConstraint((NamedConstraint) element, expressions, tables); + } + if (element instanceof KeyColumnSource) { + visitKeys((KeyColumnSource) element, expressions); + } + if (element instanceof IndexOptionSource) { + visitOptions(((IndexOptionSource) element).getIndexOptions().getStorageParameters(), + expressions); + } + } + + private static void visitConstraint(NamedConstraint element, Consumer expressions, + Consumer
tables) { + if (element instanceof NotNullConstraint) { + accept(((NotNullConstraint) element).getColumn(), expressions); + } else if (element instanceof CheckConstraint) { + accept(((CheckConstraint) element).getExpression(), expressions); + } else if (element instanceof DefaultConstraint) { + DefaultConstraint constraint = (DefaultConstraint) element; + accept(constraint.getExpression(), expressions); + accept(constraint.getColumn(), expressions); + } else if (element instanceof ExcludeConstraint) { + accept(((ExcludeConstraint) element).getExpression(), expressions); + } else if (element instanceof ForeignKeyConstraint) { + accept(((ForeignKeyConstraint) element).getTable(), tables); + } + } + + private static void visitColumn(ColumnDefinition column, Consumer expressions, + Consumer
tables) { + if (column instanceof AlterExpression.ColumnDataType + && ((AlterExpression.ColumnDataType) column).getIdentityAlterations() != null) { + return; + } + if (column.getColumnOptions() != null) { + for (ColumnOption option : column.getColumnOptions()) { + visitColumnOption(option, expressions, tables); + } + } + if (column instanceof AlterExpression.ColumnDataType) { + accept(((AlterExpression.ColumnDataType) column).getUsingExpression(), expressions); + } + } + + private static void visitColumnOption(ColumnOption option, Consumer expressions, + Consumer
tables) { + option.visitExpressions(expressions); + if (option.getKind() == ColumnOption.Kind.REFERENCE + && option.getForeignKeyReference() != null) { + accept(option.getForeignKeyReference().getTable(), tables); + } + if (option.getKind() == ColumnOption.Kind.CONSTRAINT && option.getConstraint() != null) { + visit(option.getConstraint(), expressions, tables); + } + } + + private static void visitKeys(KeyColumnSource source, Consumer expressions) { + if (source.getColumns() != null) { + for (KeyElement key : source.getColumns()) { + accept(key.getExpression(), expressions); + visitOptions(key.getOperatorClassParameters(), expressions); + } + } + } + + private static void visitOptions(List options, Consumer expressions) { + if (options != null) { + options.forEach(option -> accept(option.getValue(), expressions)); + } + } + + private static void accept(T value, Consumer consumer) { + if (value != null) { + consumer.accept(value); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 6eea66a574..7befd9beb2 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -10,12 +10,16 @@ package net.sf.jsqlparser.util; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.AliasedExpression; +import net.sf.jsqlparser.expression.RowPatternFunction; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -24,6 +28,7 @@ import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor; import net.sf.jsqlparser.expression.operators.arithmetic.Concat; import net.sf.jsqlparser.expression.operators.arithmetic.Division; +import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation; import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision; import net.sf.jsqlparser.expression.operators.arithmetic.Modulo; import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication; @@ -46,8 +51,10 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; +import net.sf.jsqlparser.expression.operators.relational.IsJsonExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; import net.sf.jsqlparser.expression.operators.relational.IsUnknownExpression; import net.sf.jsqlparser.expression.operators.relational.JsonOperator; @@ -66,57 +73,164 @@ import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.AssertStatement; +import net.sf.jsqlparser.statement.AttachStatement; import net.sf.jsqlparser.statement.Block; import net.sf.jsqlparser.statement.Commit; +import net.sf.jsqlparser.statement.ConnectStatement; +import net.sf.jsqlparser.statement.CopyStatement; import net.sf.jsqlparser.statement.CreateFunctionalStatement; +import net.sf.jsqlparser.statement.DeallocateStatement; import net.sf.jsqlparser.statement.DeclareStatement; import net.sf.jsqlparser.statement.DescribeStatement; +import net.sf.jsqlparser.statement.DetachStatement; +import net.sf.jsqlparser.statement.DisconnectStatement; +import net.sf.jsqlparser.statement.DoStatement; import net.sf.jsqlparser.statement.ExplainStatement; +import net.sf.jsqlparser.statement.ExtensionStatement; import net.sf.jsqlparser.statement.IfElseStatement; +import net.sf.jsqlparser.statement.OutputClause; +import net.sf.jsqlparser.statement.PragmaStatement; +import net.sf.jsqlparser.statement.PrepareStatement; import net.sf.jsqlparser.statement.PurgeObjectType; import net.sf.jsqlparser.statement.PurgeStatement; +import net.sf.jsqlparser.statement.ReleaseSavepointStatement; import net.sf.jsqlparser.statement.ResetStatement; +import net.sf.jsqlparser.statement.ReturningClause; import net.sf.jsqlparser.statement.RollbackStatement; import net.sf.jsqlparser.statement.SavepointStatement; import net.sf.jsqlparser.statement.SessionStatement; +import net.sf.jsqlparser.statement.SetIdentityInsertStatement; import net.sf.jsqlparser.statement.SetStatement; +import net.sf.jsqlparser.statement.SetDefaultRoleStatement; +import net.sf.jsqlparser.statement.SetPasswordStatement; +import net.sf.jsqlparser.statement.SetRoleStatement; import net.sf.jsqlparser.statement.ShowColumnsStatement; import net.sf.jsqlparser.statement.ShowStatement; +import net.sf.jsqlparser.statement.StartTransaction; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.TableMaintenanceStatement; import net.sf.jsqlparser.statement.Statements; import net.sf.jsqlparser.statement.UnsupportedStatement; import net.sf.jsqlparser.statement.UseStatement; import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterRoutine; +import net.sf.jsqlparser.statement.alter.AlterTrigger; +import net.sf.jsqlparser.statement.alter.AlterDomain; +import net.sf.jsqlparser.statement.alter.AlterExtension; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.alter.AlterPublication; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.alter.AlterSession; +import net.sf.jsqlparser.statement.alter.AlterStatistics; +import net.sf.jsqlparser.statement.alter.AlterSubscription; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; +import net.sf.jsqlparser.statement.alter.AlterTablespace; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterType; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; import net.sf.jsqlparser.statement.alter.RenameTableStatement; +import net.sf.jsqlparser.statement.alter.AlterUser; +import net.sf.jsqlparser.statement.alter.RenameUser; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; 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.accessmethod.CreateAccessMethod; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.create.database.CreateDatabase; +import net.sf.jsqlparser.statement.create.domain.CreateDomain; +import net.sf.jsqlparser.statement.create.event.AlterEvent; +import net.sf.jsqlparser.statement.create.event.CreateEvent; +import net.sf.jsqlparser.statement.create.extension.CreateExtension; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; +import net.sf.jsqlparser.statement.create.publication.CreatePublication; +import net.sf.jsqlparser.statement.create.rule.CreateRule; import net.sf.jsqlparser.statement.create.schema.CreateSchema; import net.sf.jsqlparser.statement.create.sequence.CreateSequence; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; +import net.sf.jsqlparser.statement.create.subscription.CreateSubscription; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; +import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.type.CreateType; +import net.sf.jsqlparser.statement.create.user.CreateUser; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.delete.ParenthesedDelete; import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; import net.sf.jsqlparser.statement.execute.Execute; +import net.sf.jsqlparser.statement.execute.ExecuteArgument; import net.sf.jsqlparser.statement.export.Export; +import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; import net.sf.jsqlparser.statement.grant.Grant; +import net.sf.jsqlparser.statement.grant.Revoke; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.insert.InsertBulk; +import net.sf.jsqlparser.statement.insert.InsertConflictAction; +import net.sf.jsqlparser.statement.insert.InsertDuplicateAction; import net.sf.jsqlparser.statement.insert.OracleMultiInsertBranch; import net.sf.jsqlparser.statement.insert.OracleMultiInsertClause; import net.sf.jsqlparser.statement.insert.ParenthesedInsert; +import net.sf.jsqlparser.statement.load.LoadDataStatement; import net.sf.jsqlparser.statement.lock.LockStatement; import net.sf.jsqlparser.statement.merge.Merge; +import net.sf.jsqlparser.statement.merge.MergeDelete; +import net.sf.jsqlparser.statement.merge.MergeInsert; +import net.sf.jsqlparser.statement.merge.MergeOperation; +import net.sf.jsqlparser.statement.merge.MergeOperationVisitor; +import net.sf.jsqlparser.statement.merge.MergeUpdate; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; +import net.sf.jsqlparser.statement.piped.AggregatePipeOperator; +import net.sf.jsqlparser.statement.piped.AsPipeOperator; +import net.sf.jsqlparser.statement.piped.CallPipeOperator; +import net.sf.jsqlparser.statement.piped.DropPipeOperator; +import net.sf.jsqlparser.statement.piped.ExtendPipeOperator; import net.sf.jsqlparser.statement.piped.FromQuery; +import net.sf.jsqlparser.statement.piped.JoinPipeOperator; +import net.sf.jsqlparser.statement.piped.LimitPipeOperator; +import net.sf.jsqlparser.statement.piped.OrderByPipeOperator; +import net.sf.jsqlparser.statement.piped.PipeOperator; +import net.sf.jsqlparser.statement.piped.PipeOperatorVisitor; +import net.sf.jsqlparser.statement.piped.PivotPipeOperator; +import net.sf.jsqlparser.statement.piped.RenamePipeOperator; +import net.sf.jsqlparser.statement.piped.SelectPipeOperator; +import net.sf.jsqlparser.statement.piped.SetOperationPipeOperator; +import net.sf.jsqlparser.statement.piped.SetPipeOperator; +import net.sf.jsqlparser.statement.piped.TableSamplePipeOperator; +import net.sf.jsqlparser.statement.piped.UnPivotPipeOperator; +import net.sf.jsqlparser.statement.piped.WherePipeOperator; +import net.sf.jsqlparser.statement.piped.WindowPipeOperator; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.ReassignOwned; import net.sf.jsqlparser.statement.select.AllColumns; import net.sf.jsqlparser.statement.select.AllTableColumns; import net.sf.jsqlparser.statement.select.FromItem; @@ -124,9 +238,16 @@ import net.sf.jsqlparser.statement.select.FunctionAllColumns; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; +import net.sf.jsqlparser.statement.select.LateralView; +import net.sf.jsqlparser.statement.select.Limit; +import net.sf.jsqlparser.statement.select.MatchRecognize; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; +import net.sf.jsqlparser.statement.select.Pivot; +import net.sf.jsqlparser.statement.select.PivotQuery; +import net.sf.jsqlparser.statement.select.PivotVisitor; +import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectItem; @@ -135,9 +256,13 @@ import net.sf.jsqlparser.statement.select.SetOperationList; import net.sf.jsqlparser.statement.select.TableFunction; import net.sf.jsqlparser.statement.select.TableStatement; +import net.sf.jsqlparser.statement.select.UnPivot; +import net.sf.jsqlparser.statement.select.UnPivotQuery; import net.sf.jsqlparser.statement.select.Values; import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; @@ -155,12 +280,17 @@ @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.UncommentedEmptyMethodBody"}) public class TablesNamesFinder implements SelectVisitor, FromItemVisitor, ExpressionVisitor, - SelectItemVisitor, StatementVisitor { + SelectItemVisitor, StatementVisitor, MergeOperationVisitor, + PipeOperatorVisitor, PivotVisitor { private Set tables; private boolean allowColumnProcessing = false; + private Set rowPatternVariables = java.util.Collections.emptySet(); - private List otherItemNames; + private Set otherItemNames; + // the WITH items a table reference resolves to at the current position, counted because a + // nested WITH may repeat a name of an enclosing one + private Map withItemsInScope; public static Set findTables(String sqlStr) throws JSQLParserException { TablesNamesFinder tablesNamesFinder = new TablesNamesFinder<>(); @@ -191,10 +321,6 @@ public Set getTables(Statement statement) { init(false); statement.accept(this, null); - // @todo: assess this carefully, maybe we want to remove more specifically - // only Aliases on WithItems, Parenthesed Selects and Lateral Selects - otherItemNames.forEach(tables::remove); - return tables; } @@ -210,13 +336,9 @@ public Set getTablesOrOtherSources(Statement statement) { @Override public Void visit(Select select, S context) { - List> withItemsList = select.getWithItemsList(); - if (withItemsList != null && !withItemsList.isEmpty()) { - for (WithItem withItem : withItemsList) { - withItem.accept((SelectVisitor) this, context); - } - } + visitWithItems(select.getWithItemsList(), context); select.accept((SelectVisitor) this, context); + leaveWithItems(select.getWithItemsList()); return null; } @@ -244,8 +366,20 @@ public Void visit(TrimFunction trimFunction, S context) { @Override public Void visit(RangeExpression rangeExpression, S context) { - rangeExpression.getStartExpression().accept(this, context); - rangeExpression.getEndExpression().accept(this, context); + for (Expression bound : new Expression[] {rangeExpression.getStartExpression(), + rangeExpression.getEndExpression(), rangeExpression.getStepExpression()}) { + if (bound != null) { + bound.accept(this, context); + } + } + return null; + } + + @Override + public Void visit(TernaryExpression ternaryExpression, S context) { + ternaryExpression.getCondition().accept(this, context); + ternaryExpression.getThenExpression().accept(this, context); + ternaryExpression.getElseExpression().accept(this, context); return null; } @@ -263,13 +397,69 @@ public Set getTables(Expression expr) { return tables; } + /** + * Visits the WITH items of a query and brings their names into scope, which the caller ends + * with {@link #leaveWithItems(List)} after the query. An item only sees the items before it, so + * a reference to its own or a later name is a table, unless RECURSIVE makes all of them visible + * to each other. The parser sets RECURSIVE on the first item, but it covers the list. + */ + @Override + public Void visitWithItems(List> withItemsList, S context) { + if (withItemsList != null) { + boolean recursive = withItemsList.stream().anyMatch(WithItem::isRecursive); + if (recursive) { + withItemsList.forEach(this::enterWithItem); + } + for (WithItem withItem : withItemsList) { + withItem.accept((SelectVisitor) this, context); + if (!recursive) { + enterWithItem(withItem); + } + } + } + return null; + } + + private void enterWithItem(WithItem withItem) { + if (withItem.getAlias() != null) { + withItemsInScope.merge(withItem.getAlias().getName(), 1, Integer::sum); + } + } + + private void leaveWithItems(List> withItemsList) { + if (withItemsList != null) { + for (WithItem withItem : withItemsList) { + if (withItem.getAlias() != null) { + withItemsInScope.computeIfPresent(withItem.getAlias().getName(), + (name, count) -> count > 1 ? count - 1 : null); + } + } + } + } + + /** + * The target of a DML statement is a table even if a WITH item in scope has the same name. + */ + private void visitTargetTable(Table table, S context) { + Map scope = withItemsInScope; + withItemsInScope = new HashMap<>(); + visit(table, context); + withItemsInScope = scope; + } + @Override public Void visit(WithItem withItem, S context) { if (withItem.getAlias() != null) { otherItemNames.add(withItem.getAlias().getName()); } - if (withItem.getSelect() != null) { - withItem.getSelect().accept((SelectVisitor) this, context); + if (withItem.getCycleClause() != null) { + withItem.getCycleClause().accept(this, context); + } + // dispatch any ParenthesedStatement payload (Select, Delete, Update, Insert) + withItem.accept((StatementVisitor) this, context); + // an expression alias (WITH expr AS name) may read tables on its own + if (withItem.getExpression() != null) { + withItem.getExpression().accept(this, context); } return null; } @@ -284,13 +474,23 @@ public Void visit(ParenthesedSelect select, S context) { if (select.getAlias() != null) { otherItemNames.add(select.getAlias().getName()); } - List> withItemsList = select.getWithItemsList(); - if (withItemsList != null && !withItemsList.isEmpty()) { - for (WithItem withItem : withItemsList) { - withItem.accept((SelectVisitor) this, context); - } - } + visitWithItems(select.getWithItemsList(), context); select.getSelect().accept((SelectVisitor) this, context); + visitOrderBy(select.getOrderByElements(), context); + if (select.getPivot() != null) { + select.getPivot().accept(this, context); + } + if (select.getUnPivot() != null) { + select.getUnPivot().accept(this, context); + } + visitLimit(select.getLimit(), context); + if (select.getOffset() != null) { + select.getOffset().getOffset().accept(this, context); + } + if (select.getFetch() != null) { + select.getFetch().getExpression().accept(this, context); + } + leaveWithItems(select.getWithItemsList()); return null; } @@ -301,38 +501,113 @@ public void visit(ParenthesedSelect parenthesedSelect) { @Override public Void visit(PlainSelect plainSelect, S context) { - List> withItemsList = plainSelect.getWithItemsList(); - if (withItemsList != null && !withItemsList.isEmpty()) { - for (WithItem withItem : withItemsList) { - withItem.accept((SelectVisitor) this, context); - } + Set previous = rowPatternVariables; + try { + rowPatternVariables = java.util.Collections.emptySet(); + return visitPlainSelectBody(plainSelect, context); + } finally { + rowPatternVariables = previous; } - if (plainSelect.getSelectItems() != null) { - for (SelectItem item : plainSelect.getSelectItems()) { - item.accept(this, context); - } + } + + private Void visitPlainSelectBody(PlainSelect plainSelect, S context) { + visitWithItems(plainSelect.getWithItemsList(), context); + if (plainSelect.getDistinct() != null) { + visitSelectItems(plainSelect.getDistinct().getOnSelectItems(), context); } + visitTables(plainSelect.getIntoTables(), context); + + visitSelectItems(plainSelect.getSelectItems(), context); + visitFromItem(plainSelect.getFromItem(), context); - if (plainSelect.getFromItem() != null) { - plainSelect.getFromItem().accept(this, context); + if (plainSelect.getLateralViews() != null) { + for (LateralView lateralView : plainSelect.getLateralViews()) { + lateralView.getGeneratorFunction().accept(this, context); + } } visitJoins(plainSelect.getJoins(), context); - if (plainSelect.getPreWhere() != null) { - plainSelect.getPreWhere().accept(this, context); + visitExpression(plainSelect.getPreWhere(), context); + visitExpression(plainSelect.getWhere(), context); + + visitPreferringClause(plainSelect.getPreferringClause(), context); + visit(plainSelect.getGroupBy(), context); + + visitExpression(plainSelect.getHaving(), context); + + visitExpression(plainSelect.getQualify(), context); + + visitExpression(plainSelect.getOracleHierarchical(), context); + + if (plainSelect.getWindowDefinitions() != null) { + for (WindowDefinition windowDefinition : plainSelect.getWindowDefinitions()) { + visitExpressions(windowDefinition.getPartitionExpressionList(), context); + visitOrderBy(windowDefinition.getOrderByElements(), context); + } + } + + if (plainSelect.getPivot() != null) { + plainSelect.getPivot().accept(this, context); } - if (plainSelect.getWhere() != null) { - plainSelect.getWhere().accept(this, context); + if (plainSelect.getUnPivot() != null) { + plainSelect.getUnPivot().accept(this, context); } - if (plainSelect.getHaving() != null) { - plainSelect.getHaving().accept(this, context); + visitOrderBy(plainSelect.getOrderByElements(), context); + visitLimit(plainSelect.getLimit(), context); + visitLimit(plainSelect.getLimitBy(), context); + visitSelectPagination(plainSelect, context); + visitUpdateSets(plainSelect.getSettings(), context); + visitFromItem(plainSelect.getIntoTempTable(), context); + leaveWithItems(plainSelect.getWithItemsList()); + return null; + } + + @Override + public Void visit(PivotQuery pivotQuery, S context) { + visitWithItems(pivotQuery.getWithItemsList(), context); + visitFromItem(pivotQuery.getFromItem(), context); + visitExpressions(pivotQuery.getOnExpressions(), context); + visitSelectItems(pivotQuery.getUsingItems(), context); + visitExpressions(pivotQuery.getGroupByExpressions(), context); + visitOrderBy(pivotQuery.getOrderByElements(), context); + visitLimit(pivotQuery.getLimit(), context); + + visitSelectPagination(pivotQuery, context); + leaveWithItems(pivotQuery.getWithItemsList()); + return null; + } + + @Override + public Void visit(UnPivotQuery unPivotQuery, S context) { + visitWithItems(unPivotQuery.getWithItemsList(), context); + visitFromItem(unPivotQuery.getFromItem(), context); + visitExpressions(unPivotQuery.getOnExpressions(), context); + visitExpressions(unPivotQuery.getValueColumns(), context); + leaveWithItems(unPivotQuery.getWithItemsList()); + return null; + } + + @Override + public void visit(UnPivotQuery unPivotQuery) { + SelectVisitor.super.visit(unPivotQuery); + } + + private void visitSelectItems(List> selectItems, S context) { + if (selectItems != null) { + for (SelectItem item : selectItems) { + item.accept(this, context); + } } + } - if (plainSelect.getOracleHierarchical() != null) { - plainSelect.getOracleHierarchical().accept(this, context); + private void visitSelectPagination(Select select, S context) { + if (select.getOffset() != null) { + select.getOffset().getOffset().accept(this, context); + } + if (select.getFetch() != null && select.getFetch().getExpression() != null) { + select.getFetch().getExpression().accept(this, context); } - return null; } @Override @@ -340,6 +615,11 @@ public void visit(PlainSelect plainSelect) { SelectVisitor.super.visit(plainSelect); } + @Override + public void visit(PivotQuery pivotQuery) { + SelectVisitor.super.visit(pivotQuery); + } + /** * Override to adapt the tableName generation (e.g. with / without schema). * @@ -350,12 +630,47 @@ protected String extractTableName(Table table) { return table.getFullyQualifiedName(); } + @Override + public Void visit(RowPatternFunction function, S context) { + return function.getFunction().accept(this, context); + } + + @Override + public Void visit(MatchRecognize matchRecognize, S context) { + matchRecognize.getInput().accept(this, context); + if (matchRecognize.getPivot() != null) { + matchRecognize.getPivot().accept(this, context); + } + if (matchRecognize.getUnPivot() != null) { + matchRecognize.getUnPivot().accept(this, context); + } + matchRecognize.getPartitionBy().forEach(expression -> expression.accept(this, context)); + matchRecognize.getOrderByElements() + .forEach(order -> order.getExpression().accept(this, context)); + Set previous = rowPatternVariables; + try { + rowPatternVariables = matchRecognize.getPatternVariableNames(); + matchRecognize.forEachPatternExpression(expression -> expression.accept(this, context)); + } finally { + rowPatternVariables = previous; + } + return null; + } + @Override public Void visit(Table table, S context) { String tableWholeName = extractTableName(table); - if (!otherItemNames.contains(tableWholeName)) { + if (table.isTableVariable()) { + otherItemNames.add(tableWholeName); + } else if (!withItemsInScope.containsKey(table.getFullyQualifiedName())) { tables.add(tableWholeName); } + if (table.getPivot() != null) { + table.getPivot().accept(this, context); + } + if (table.getUnPivot() != null) { + table.getUnPivot().accept(this, context); + } return null; } @@ -394,9 +709,15 @@ public Void visit(OverlapsCondition overlapsCondition, S context) { @Override public Void visit(Column tableColumn, S context) { if (allowColumnProcessing && tableColumn.getTable() != null - && tableColumn.getTable().getName() != null) { + && tableColumn.getTable().getName() != null + && !otherItemNames.contains(extractTableName(tableColumn.getTable())) + && !rowPatternVariables.contains( + MatchRecognize.normalizeVariableName(tableColumn.getTable().getName()))) { visit(tableColumn.getTable(), context); } + if (tableColumn.getArrayConstructor() != null) { + tableColumn.getArrayConstructor().accept(this, context); + } return null; } @@ -412,6 +733,12 @@ public Void visit(IntegerDivision division, S context) { return null; } + @Override + public Void visit(Exponentiation exponentiation, S context) { + visitBinaryExpression(exponentiation); + return null; + } + @Override public Void visit(DoubleValue doubleValue, S context) { @@ -434,9 +761,32 @@ public Void visit(Function function, S context) { if (exprList != null) { visit(exprList, context); } + exprList = function.getNamedParameters(); + if (exprList != null) { + visit(exprList, context); + } + // an attribute column names a field of the result, only an attribute function is visited + if (function.getAttribute() != function.getAttributeColumn()) { + visitExpression((Expression) function.getAttribute(), context); + } + visitOrderBy(function.getOrderByElements(), context); + visitAggregateModifiers(function.getKeep(), function.getHavingClause(), + function.getKeywordArguments(), function.getLimit(), context); return null; } + private void visitAggregateModifiers(KeepExpression keep, Function.HavingClause having, + List keywordArguments, Limit limit, S context) { + visitExpression(keep, context); + visitExpression(having, context); + if (keywordArguments != null) { + for (Function.KeywordArgument keywordArgument : keywordArguments) { + visitExpression(keywordArgument.getExpression(), context); + } + } + visitLimit(limit, context); + } + @Override public Void visit(GreaterThan greaterThan, S context) { visitBinaryExpression(greaterThan); @@ -472,7 +822,8 @@ public Void visit(ExcludesExpression excludesExpression, S context) { @Override public Void visit(FullTextSearch fullTextSearch, S context) { - + visitExpressions(fullTextSearch.getMatchColumns(), context); + visitExpression(fullTextSearch.getAgainstValue(), context); return null; } @@ -495,8 +846,14 @@ public Void visit(IsBooleanExpression isBooleanExpression, S context) { } @Override - public Void visit(IsUnknownExpression isUnknownExpression, S context) { + public Void visit(IsJsonExpression expression, S context) { + expression.getLeftExpression().accept(this, context); + return null; + } + @Override + public Void visit(IsUnknownExpression isUnknownExpression, S context) { + isUnknownExpression.getLeftExpression().accept(this, context); return null; } @@ -509,6 +866,7 @@ public Void visit(JdbcParameter jdbcParameter, S context) { @Override public Void visit(LikeExpression likeExpression, S context) { visitBinaryExpression(likeExpression); + visitExpression(likeExpression.getEscape(), context); return null; } @@ -756,50 +1114,59 @@ public Void visit(AnalyticExpression analytic, S context) { if (analytic.getOffset() != null) { analytic.getOffset().accept(this, context); } - if (analytic.getKeep() != null) { - analytic.getKeep().accept(this, context); + visitAggregateModifiers(analytic.getKeep(), analytic.getHavingClause(), + analytic.getKeywordArguments(), analytic.getLimit(), context); + if (analytic.getFilterExpression() != null) { + analytic.getFilterExpression().accept(this, context); + } + if (analytic.getPartitionExpressionList() != null) { + visit(analytic.getPartitionExpressionList(), context); } if (analytic.getFuncOrderBy() != null) { + for (OrderByElement element : analytic.getFuncOrderBy()) { + element.getExpression().accept(this, context); + } + } + if (analytic.getOrderByElements() != null) { for (OrderByElement element : analytic.getOrderByElements()) { element.getExpression().accept(this, context); } } + visitWindowElement(analytic.getWindowElement(), context); + return null; + } - if (analytic.getWindowElement() != null) { - if (analytic.getWindowElement().getRange() != null) { - if (analytic.getWindowElement().getRange().getStart() != null - && analytic.getWindowElement().getRange().getStart() - .getExpression() != null) { - analytic.getWindowElement().getRange().getStart().getExpression().accept(this, - context); + private void visitWindowElement(WindowElement windowElement, S context) { + if (windowElement != null) { + if (windowElement.getRange() != null) { + if (windowElement.getRange().getStart() != null) { + visitExpression(windowElement.getRange().getStart().getExpression(), context); } - if (analytic.getWindowElement().getRange().getEnd() != null - && analytic.getWindowElement().getRange().getEnd() - .getExpression() != null) { - analytic.getWindowElement().getRange().getEnd().getExpression().accept(this, - context); + if (windowElement.getRange().getEnd() != null) { + visitExpression(windowElement.getRange().getEnd().getExpression(), context); } } - if (analytic.getWindowElement().getOffset() != null - && analytic.getWindowElement().getOffset().getExpression() != null) { - analytic.getWindowElement().getOffset().getExpression().accept(this, context); + if (windowElement.getOffset() != null) { + visitExpression(windowElement.getOffset().getExpression(), context); } - } - return null; } @Override public Void visit(SetOperationList list, S context) { - List> withItemsList = list.getWithItemsList(); - if (withItemsList != null && !withItemsList.isEmpty()) { - for (WithItem withItem : withItemsList) { - withItem.accept((SelectVisitor) this, context); - } - } + visitWithItems(list.getWithItemsList(), context); for (Select selectBody : list.getSelects()) { selectBody.accept((SelectVisitor) this, context); } + visitOrderBy(list.getOrderByElements(), context); + visitLimit(list.getLimit(), context); + if (list.getOffset() != null) { + list.getOffset().getOffset().accept(this, context); + } + if (list.getFetch() != null) { + list.getFetch().getExpression().accept(this, context); + } + leaveWithItems(list.getWithItemsList()); return null; } @@ -832,7 +1199,9 @@ public void visit(LateralSubSelect lateralSubSelect) { @Override public Void visit(TableStatement tableStatement, S context) { + visitWithItems(tableStatement.getWithItemsList(), context); tableStatement.getTable().accept(this, null); + leaveWithItems(tableStatement.getWithItemsList()); return null; } @@ -843,9 +1212,175 @@ public void visit(TableStatement tableStatement) { @Override public Void visit(FromQuery fromQuery, S context) { + visitWithItems(fromQuery.getWithItemsList(), context); + if (fromQuery.getFromItem() != null) { + fromQuery.getFromItem().accept(this, context); + } + if (fromQuery.getLateralViews() != null) { + for (LateralView lateralView : fromQuery.getLateralViews()) { + lateralView.getGeneratorFunction().accept(this, context); + } + } + visitJoins(fromQuery.getJoins(), context); + for (PipeOperator pipeOperator : fromQuery.getPipeOperators()) { + pipeOperator.accept(this, null); + } + leaveWithItems(fromQuery.getWithItemsList()); + return null; + } + + @Override + public Void visit(Pivot pivot, S context) { + visitSelectItems(pivot.getFunctionItems(), context); + visitExpressions(pivot.getForColumns(), context); + visitSelectItems(pivot.getSingleInItems(), context); + visitSelectItems(pivot.getMultiInItems(), context); + return null; + } + + @Override + public Void visit(PivotXml pivotXml, S context) { + visit((Pivot) pivotXml, context); + if (pivotXml.getInSelect() != null) { + pivotXml.getInSelect().accept((SelectVisitor) this, context); + } + return null; + } + + @Override + public Void visit(UnPivot unpivot, S context) { + for (Column column : unpivot.getUnPivotClause()) { + column.accept(this, context); + } + for (Column column : unpivot.getUnPivotForClause()) { + column.accept(this, context); + } + visitSelectItems(unpivot.getUnPivotInClause(), context); + return null; + } + + @Override + public Void visit(AggregatePipeOperator aggregate, Void context) { + for (SelectItem selectItem : aggregate.getSelectItems()) { + selectItem.accept(this, context); + } + for (SelectItem groupItem : aggregate.getGroupItems()) { + groupItem.accept(this, context); + } + return null; + } + + @Override + public Void visit(AsPipeOperator as, Void context) { + if (as.getAlias() != null) { + otherItemNames.add(as.getAlias().getName()); + } + return null; + } + + @Override + public Void visit(CallPipeOperator call, Void context) { + visit(call.getTableFunction(), context); + return null; + } + + @Override + public Void visit(DropPipeOperator drop, Void context) { + drop.getColumns().accept(this, context); + return null; + } + + @Override + public Void visit(ExtendPipeOperator extend, Void context) { + return visit((SelectPipeOperator) extend, context); + } + + @Override + public Void visit(JoinPipeOperator joinPipeOperator, Void context) { + visitJoins(List.of(joinPipeOperator.getJoin()), context); + return null; + } + + @Override + public Void visit(LimitPipeOperator limit, Void context) { + limit.getLimitExpression().accept(this, context); + if (limit.getOffsetExpression() != null) { + limit.getOffsetExpression().accept(this, context); + } + return null; + } + + @Override + public Void visit(OrderByPipeOperator orderBy, Void context) { + for (OrderByElement element : orderBy.getOrderByElements()) { + element.getExpression().accept(this, context); + } + return null; + } + + @Override + public Void visit(PivotPipeOperator pivot, Void context) { + pivot.getAggregateExpression().accept(this, context); + for (SelectItem pivotColumn : pivot.getPivotColumns()) { + pivotColumn.accept(this, context); + } + return null; + } + + @Override + public Void visit(RenamePipeOperator rename, Void context) { + return visit((SelectPipeOperator) rename, context); + } + + @Override + public Void visit(SelectPipeOperator select, Void context) { + for (SelectItem selectItem : select.getSelectItems()) { + selectItem.accept(this, context); + } + return null; + } + + @Override + public Void visit(SetPipeOperator set, Void context) { + for (UpdateSet updateSet : set.getUpdateSets()) { + updateSet.getColumns().accept(this, context); + updateSet.getValues().accept(this, context); + } + return null; + } + + @Override + public Void visit(TableSamplePipeOperator tableSample, Void context) { return null; } + @Override + public Void visit(SetOperationPipeOperator setOperation, Void context) { + for (ParenthesedSelect select : setOperation.getSelects()) { + select.accept((SelectVisitor) this, context); + } + return null; + } + + @Override + public Void visit(UnPivotPipeOperator unPivot, Void context) { + for (SelectItem pivotColumn : unPivot.getPivotColumns()) { + pivotColumn.accept(this, context); + } + return null; + } + + @Override + public Void visit(WherePipeOperator where, Void context) { + where.getExpression().accept(this, context); + return null; + } + + @Override + public Void visit(WindowPipeOperator window, Void context) { + return visit((SelectPipeOperator) window, context); + } + @Override public Void visit(DateUnitExpression dateUnitExpression, S context) { return null; @@ -860,9 +1395,11 @@ public Void visit(DateUnitExpression dateUnitExpression, S context) { * @param allowColumnProcessing */ protected void init(boolean allowColumnProcessing) { - otherItemNames = new ArrayList(); + otherItemNames = new LinkedHashSet(); + withItemsInScope = new HashMap<>(); tables = new HashSet<>(); this.allowColumnProcessing = allowColumnProcessing; + this.rowPatternVariables = java.util.Collections.emptySet(); } @Override @@ -902,6 +1439,9 @@ public Void visit(JsonExpression jsonExpr, S context) { if (jsonExpr.getExpression() != null) { jsonExpr.getExpression().accept(this, context); } + for (Expression ident : jsonExpr.getIdents()) { + visitExpression(ident, context); + } return null; } @@ -911,6 +1451,12 @@ public Void visit(JsonOperator jsonExpr, S context) { return null; } + @Override + public Void visit(Intersects intersects, S context) { + visitBinaryExpression(intersects); + return null; + } + @Override public Void visit(AllColumns allColumns, S context) { @@ -925,7 +1471,7 @@ public Void visit(AllTableColumns allTableColumns, S context) { @Override public Void visit(FunctionAllColumns functionAllColumns, S context) { - + functionAllColumns.getFunction().accept(this, context); return null; } @@ -967,19 +1513,21 @@ public Void visit(NumericBind numericBind, S context) { @Override public Void visit(KeepExpression keepExpression, S context) { - + visitOrderBy(keepExpression.getOrderByElements(), context); return null; } @Override public Void visit(MySQLGroupConcat groupConcat, S context) { - + visitExpressions(groupConcat.getExpressionList(), context); + visitOrderBy(groupConcat.getOrderByElements(), context); return null; } @Override public Void visit(Delete delete, S context) { - visit(delete.getTable(), context); + visitWithItems(delete.getWithItemsList(), context); + visitTargetTable(delete.getTable(), context); if (delete.getUsingFromItemList() != null) { for (FromItem usingFromItem : delete.getUsingFromItemList()) { @@ -989,9 +1537,17 @@ public Void visit(Delete delete, S context) { visitJoins(delete.getJoins(), context); + if (delete.getForPortionClause() != null) { + delete.getForPortionClause().accept(this, context); + } if (delete.getWhere() != null) { delete.getWhere().accept(this, context); } + visitOrderBy(delete.getOrderByElements(), context); + visitLimit(delete.getLimit(), context); + visitOutputClause(delete.getOutputClause(), context); + visitReturningClause(delete.getReturningClause(), context); + leaveWithItems(delete.getWithItemsList()); return null; } @@ -1012,20 +1568,14 @@ public Void visit(SessionStatement sessionStatement, S context) { @Override public Void visit(Update update, S context) { - if (update.getWithItemsList() != null) { - for (WithItem withItem : update.getWithItemsList()) { - withItem.accept((SelectVisitor) this, context); - } - } - - visit(update.getTable(), context); + visitWithItems(update.getWithItemsList(), context); - if (update.getStartJoins() != null) { - for (Join join : update.getStartJoins()) { - join.getRightItem().accept(this, context); - } + if (!update.isTargetTableAlias()) { + visitTargetTable(update.getTable(), context); } + visitJoins(update.getStartJoins(), context); + if (update.getUpdateSets() != null) { for (UpdateSet updateSet : update.getUpdateSets()) { updateSet.getColumns().accept(this, context); @@ -1037,18 +1587,19 @@ public Void visit(Update update, S context) { update.getFromItem().accept(this, context); } - if (update.getJoins() != null) { - for (Join join : update.getJoins()) { - join.getRightItem().accept(this, context); - for (Expression expression : join.getOnExpressions()) { - expression.accept(this, context); - } - } - } + visitJoins(update.getJoins(), context); + if (update.getForPortionClause() != null) { + update.getForPortionClause().accept(this, context); + } if (update.getWhere() != null) { update.getWhere().accept(this, context); } + visitOrderBy(update.getOrderByElements(), context); + visitLimit(update.getLimit(), context); + visitOutputClause(update.getOutputClause(), context); + visitReturningClause(update.getReturningClause(), context); + leaveWithItems(update.getWithItemsList()); return null; } @@ -1073,39 +1624,90 @@ public Void visit(Insert insert, S context) { continue; } for (OracleMultiInsertClause clause : branch.getClauses()) { - visit(clause.getTable(), context); + visitTargetTable(clause.getTable(), context); if (clause.getSelect() != null) { visit(clause.getSelect(), context); } } } } else if (insert.getTable() != null) { - visit(insert.getTable(), context); + visitTargetTable(insert.getTable(), context); } - if (insert.getWithItemsList() != null) { - for (WithItem withItem : insert.getWithItemsList()) { - withItem.accept((SelectVisitor) this, context); - } + visitWithItems(insert.getWithItemsList(), context); + if (insert.getSetUpdateSets() != null) { + visitUpdateSets(insert.getSetUpdateSets(), context); + } + if (insert.getDuplicateAction() != null) { + visitInsertAction(insert.getDuplicateAction(), context); } + if (insert.getConflictAction() != null) { + visitInsertAction(insert.getConflictAction(), context); + } + if (insert.getConflictTarget() != null) { + insert.getConflictTarget().accept(this, context); + } + visitOutputClause(insert.getOutputClause(), context); + visitReturningClause(insert.getReturningClause(), context); if (insert.getSelect() != null) { visit(insert.getSelect(), context); } + leaveWithItems(insert.getWithItemsList()); return null; } - @Override - public Void visit(ParenthesedInsert insert, S context) { - return visit(insert.getInsert(), context); + private void visitInsertAction(InsertDuplicateAction action, S context) { + visitUpdateSets(action.getUpdateSets(), context); + if (action.getWhereExpression() != null) { + action.getWhereExpression().accept(this, context); + } } - @Override - public void visit(Insert insert) { + private void visitInsertAction(InsertConflictAction action, S context) { + visitUpdateSets(action.getUpdateSets(), context); + if (action.getWhereExpression() != null) { + action.getWhereExpression().accept(this, context); + } + } + + @Override + public Void visitOutputClause(OutputClause outputClause, S context) { + if (outputClause != null) { + if (outputClause.getSelectItemList() != null) { + for (SelectItem selectItem : outputClause.getSelectItemList()) { + selectItem.accept(this, context); + } + } + if (outputClause.getOutputTable() != null) { + visit(outputClause.getOutputTable(), context); + } + if (outputClause.getTableVariable() != null) { + outputClause.getTableVariable().accept(this, context); + } + } + return null; + } + + private void visitReturningClause(ReturningClause returningClause, S context) { + if (returningClause != null) { + for (SelectItem selectItem : returningClause) { + selectItem.accept(this, context); + } + } + } + + @Override + public Void visit(ParenthesedInsert insert, S context) { + return visit(insert.getInsert(), context); + } + + @Override + public void visit(Insert insert) { StatementVisitor.super.visit(insert); } @Override public Void visit(Analyze analyze, S context) { - visit(analyze.getTable(), context); + analyze.getTables().forEach(table -> visit(table, context)); return null; } @@ -1114,9 +1716,20 @@ public void visit(Analyze analyze) { StatementVisitor.super.visit(analyze); } + @Override + public Void visit(TableMaintenanceStatement statement, S context) { + statement.getTables().forEach(table -> visit(table, context)); + return null; + } + + @Override + public void visit(TableMaintenanceStatement statement) { + StatementVisitor.super.visit(statement); + } + @Override public Void visit(Drop drop, S context) { - visit(drop.getName(), context); + drop.visitTables(table -> visit(table, context)); return null; } @@ -1125,9 +1738,15 @@ public void visit(Drop drop) { StatementVisitor.super.visit(drop); } + @Override + public Void visit(ReassignOwned statement, S context) { + // Role names do not refer to tables. + return null; + } + @Override public Void visit(Truncate truncate, S context) { - visit(truncate.getTable(), context); + truncate.getTables().forEach(table -> visit(table, context)); return null; } @@ -1138,7 +1757,22 @@ public void visit(Truncate truncate) { @Override public Void visit(CreateIndex createIndex, S context) { - throwUnsupported(createIndex); + TableDefinitionTraversal.visit(createIndex, + expression -> expression.accept(this, context), + table -> visit(table, context)); + return null; + } + + @Override + public Void visit(DoStatement statement, S context) { + throwUnsupported(statement); + return null; + } + + @Override + public Void visit(InsertBulk statement, S context) { + visit(statement.getTable(), context); + statement.visitExpressions(value -> value.accept(this, context)); return null; } @@ -1149,7 +1783,7 @@ public void visit(CreateIndex createIndex) { @Override public Void visit(CreateSchema createSchema, S context) { - throwUnsupported(createSchema); + createSchema.getStatements().forEach(element -> element.accept(this, context)); return null; } @@ -1158,9 +1792,72 @@ public void visit(CreateSchema createSchema) { StatementVisitor.super.visit(createSchema); } + @Override + public Void visit(CreateDatabase createDatabase, S context) { + throwUnsupported(createDatabase); + return null; + } + + @Override + public void visit(CreateDatabase createDatabase) { + StatementVisitor.super.visit(createDatabase); + } + + @Override + public Void visit(CreateUser createUser, S context) { + return null; + } + + @Override + public void visit(CreateUser createUser) { + StatementVisitor.super.visit(createUser); + } + + // Account and role names are not tables. + + @Override + public Void visit(AlterUser alterUser, S context) { + return null; + } + + @Override + public Void visit(RenameUser renameUser, S context) { + return null; + } + + @Override + public Void visit(SetRoleStatement setRole, S context) { + return null; + } + + @Override + public Void visit(SetDefaultRoleStatement setDefaultRole, S context) { + return null; + } + + @Override + public Void visit(SetPasswordStatement setPassword, S context) { + return null; + } + + @Override + public Void visit(CreateEvent createEvent, S context) { + if (createEvent.getBody() != null) { + createEvent.getBody().accept(this, context); + } + return null; + } + + @Override + public void visit(CreateEvent createEvent) { + StatementVisitor.super.visit(createEvent); + } + @Override public Void visit(CreateTable create, S context) { visit(create.getTable(), null); + TableDefinitionTraversal.visit(create, + expression -> expression.accept(this, context), table -> visit(table, context)); if (create.getSelect() != null) { create.getSelect().accept((SelectVisitor) this, context); } @@ -1172,6 +1869,45 @@ public void visit(CreateTable createTable) { StatementVisitor.super.visit(createTable); } + @Override + public Void visit(CreateTrigger createTrigger, S context) { + createTrigger.visit(t -> t.accept(this, context), e -> e.accept(this, context)); + if (createTrigger.getBody() != null) { + createTrigger.getBody().accept(this, context); + } + return null; + } + + @Override + public void visit(CreateTrigger createTrigger) { + StatementVisitor.super.visit(createTrigger); + } + + @Override + public Void visit(CreateEventTrigger statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterEventTrigger statement, S context) { + return null; + } + + @Override + public Void visit(Reindex statement, S context) { + if (statement.getTable() != null) { + visit(statement.getTable(), context); + } + return null; + } + + @Override + public Void visit(SecurityLabel statement, S context) { + statement.visitRelations(table -> visit(table, context)); + return null; + } + @Override public Void visit(CreateView create, S context) { visit(create.getView(), null); @@ -1188,12 +1924,31 @@ public void visit(CreateView createView) { @Override public Void visit(Alter alter, S context) { + for (net.sf.jsqlparser.statement.alter.AlterExpression action : alter + .getAlterExpressions()) { + TableDefinitionTraversal.visit(action, + expression -> expression.accept(this, context), + table -> visit(table, context)); + } return alter.getTable().accept(this, context); } @Override public void visit(Alter alter) { - alter.getTable().accept(this, null); + StatementVisitor.super.visit(alter); + } + + @Override + public Void visit(AlterEvent alterEvent, S context) { + if (alterEvent.getBody() != null) { + alterEvent.getBody().accept(this, context); + } + return null; + } + + @Override + public void visit(AlterEvent alterEvent) { + StatementVisitor.super.visit(alterEvent); } @Override @@ -1215,6 +1970,11 @@ public Void visit(Execute execute, S context) { return null; } + @Override + public Void visit(ExecuteArgument argument, S context) { + return argument.getExpression().accept(this, context); + } + @Override public void visit(Execute execute) { StatementVisitor.super.visit(execute); @@ -1222,7 +1982,14 @@ public void visit(Execute execute) { @Override public Void visit(SetStatement setStatement, S context) { - throwUnsupported(setStatement); + for (int i = 0; i < setStatement.getCount(); i++) { + List expressions = setStatement.getExpressions(i); + if (expressions != null) { + for (Expression expression : expressions) { + expression.accept(this, context); + } + } + } return null; } @@ -1244,7 +2011,14 @@ public void visit(ResetStatement reset) { @Override public Void visit(ShowColumnsStatement showColumnsStatement, S context) { - throwUnsupported(showColumnsStatement); + Table table = showColumnsStatement.getTable(); + if (table != null && showColumnsStatement.getDbName() != null) { + // MySQL: the trailing FROM db names the database of the table + table = new Table(showColumnsStatement.getDbName(), table.getName()); + } + if (table != null) { + visit(table, context); + } return null; } @@ -1278,6 +2052,11 @@ public Void visit(RowGetExpression rowGetExpression, S context) { return null; } + @Override + public Void visit(MethodCallExpression methodCall, S context) { + return ExpressionVisitor.super.visit(methodCall, context); + } + @Override public Void visit(HexValue hexValue, S context) { return null; @@ -1285,16 +2064,62 @@ public Void visit(HexValue hexValue, S context) { @Override public Void visit(Merge merge, S context) { - visit(merge.getTable(), context); - if (merge.getWithItemsList() != null) { - for (WithItem withItem : merge.getWithItemsList()) { - withItem.accept((SelectVisitor) this, context); - } - } + visitTargetTable(merge.getTable(), context); + visitWithItems(merge.getWithItemsList(), context); if (merge.getFromItem() != null) { merge.getFromItem().accept(this, context); } + + if (merge.getOnCondition() != null) { + merge.getOnCondition().accept(this, context); + } + + if (merge.getOperations() != null) { + for (MergeOperation operation : merge.getOperations()) { + operation.accept(this, context); + } + } + visitOutputClause(merge.getOutputClause(), context); + visitReturningClause(merge.getReturningClause(), context); + leaveWithItems(merge.getWithItemsList()); + return null; + } + + @Override + public Void visit(MergeDelete mergeDelete, S context) { + if (mergeDelete.getAndPredicate() != null) { + mergeDelete.getAndPredicate().accept(this, context); + } + return null; + } + + @Override + public Void visit(MergeInsert mergeInsert, S context) { + if (mergeInsert.getAndPredicate() != null) { + mergeInsert.getAndPredicate().accept(this, context); + } + if (mergeInsert.getValues() != null) { + mergeInsert.getValues().accept(this, context); + } + if (mergeInsert.getWhereCondition() != null) { + mergeInsert.getWhereCondition().accept(this, context); + } + return null; + } + + @Override + public Void visit(MergeUpdate mergeUpdate, S context) { + if (mergeUpdate.getAndPredicate() != null) { + mergeUpdate.getAndPredicate().accept(this, context); + } + visitUpdateSets(mergeUpdate.getUpdateSets(), context); + if (mergeUpdate.getWhereCondition() != null) { + mergeUpdate.getWhereCondition().accept(this, context); + } + if (mergeUpdate.getDeleteWhereCondition() != null) { + mergeUpdate.getDeleteWhereCondition().accept(this, context); + } return null; } @@ -1312,7 +2137,8 @@ public Void visit(OracleHint hint, S context) { public Void visit(TableFunction tableFunction, S context) { if (tableFunction.getFunctions() != null) { for (var function : tableFunction.getFunctions()) { - visit(function, null); + // dispatch dynamically, XMLTABLE and JSON_TABLE are Function subclasses + function.accept(this, context); } } return null; @@ -1325,7 +2151,10 @@ public void visit(TableFunction tableFunction) { @Override public Void visit(AlterView alterView, S context) { - throwUnsupported(alterView); + visit(alterView.getView(), context); + if (alterView.getSelect() != null) { + alterView.getSelect().accept((SelectVisitor) this, context); + } return null; } @@ -1355,6 +2184,16 @@ public Void visit(DateTimeLiteralExpression literal, S context) { return null; } + @Override + public Void visit(StartTransaction statement, S context) { + return null; + } + + @Override + public Void visit(ReleaseSavepointStatement statement, S context) { + return null; + } + @Override public Void visit(Commit commit, S context) { return null; @@ -1374,6 +2213,9 @@ public Void visit(Upsert upsert, S context) { if (upsert.getSelect() != null) { visit(upsert.getSelect(), context); } + if (upsert.getDuplicateAction() != null) { + visitInsertAction(upsert.getDuplicateAction(), context); + } return null; } @@ -1382,6 +2224,11 @@ public void visit(Upsert upsert) { StatementVisitor.super.visit(upsert); } + @Override + public Void visit(SetIdentityInsertStatement statement, S context) { + return statement.getTable().accept(this, context); + } + @Override public Void visit(UseStatement use, S context) { return null; @@ -1437,15 +2284,7 @@ public void visit(Block block) { @Override public Void visit(Comment comment, S context) { - if (comment.getTable() != null) { - visit(comment.getTable(), context); - } - if (comment.getColumn() != null) { - Table table = comment.getColumn().getTable(); - if (table != null) { - visit(table, context); - } - } + comment.visitRelations(table -> visit(table, context)); return null; } @@ -1456,7 +2295,17 @@ public void visit(Comment comment) { @Override public Void visit(Values values, S context) { + visitWithItems(values.getWithItemsList(), context); values.getExpressions().accept(this, context); + visitOrderBy(values.getOrderByElements(), context); + visitLimit(values.getLimit(), context); + if (values.getOffset() != null) { + values.getOffset().getOffset().accept(this, context); + } + if (values.getFetch() != null) { + values.getFetch().getExpression().accept(this, context); + } + leaveWithItems(values.getWithItemsList()); return null; } @@ -1467,7 +2316,11 @@ public void visit(Values values) { @Override public Void visit(DescribeStatement describe, S context) { - describe.getTable().accept(this, context); + if (describe.getSelect() != null) { + describe.getSelect().accept((SelectVisitor) this, context); + } else { + describe.getTable().accept(this, context); + } return null; } @@ -1528,6 +2381,7 @@ public void visit(DeclareStatement declareStatement) { @Override public Void visit(Grant grant, S context) { + grant.getClause().visit(t -> t.accept(this, context), e -> e.accept(this, context)); return null; } @@ -1539,7 +2393,7 @@ public void visit(Grant grant) { @Override public Void visit(ArrayExpression array, S context) { array.getObjExpression().accept(this, context); - if (array.getStartIndexExpression() != null) { + if (array.getIndexExpression() != null) { array.getIndexExpression().accept(this, context); } if (array.getStartIndexExpression() != null) { @@ -1548,6 +2402,9 @@ public Void visit(ArrayExpression array, S context) { if (array.getStopIndexExpression() != null) { array.getStopIndexExpression().accept(this, context); } + if (array.getStepExpression() != null) { + array.getStepExpression().accept(this, context); + } return null; } @@ -1559,9 +2416,21 @@ public Void visit(ArrayConstructor array, S context) { return null; } + @Override + public Void visit(MapExpression mapExpression, S context) { + for (Map.Entry entry : mapExpression.getEntries()) { + entry.getKey().accept(this, context); + entry.getValue().accept(this, context); + } + return null; + } + @Override public Void visit(CreateSequence createSequence, S context) { - throwUnsupported(createSequence); + if (createSequence.getSequence().getOwnership() != null + && !createSequence.getSequence().getOwnership().isNone()) { + visit(createSequence.getSequence().getOwnership().getColumn().getTable(), context); + } return null; } @@ -1572,7 +2441,10 @@ public void visit(CreateSequence createSequence) { @Override public Void visit(AlterSequence alterSequence, S context) { - throwUnsupported(alterSequence); + if (alterSequence.getSequence().getOwnership() != null + && !alterSequence.getSequence().getOwnership().isNone()) { + visit(alterSequence.getSequence().getOwnership().getColumn().getTable(), context); + } return null; } @@ -1603,6 +2475,32 @@ public void visit(ShowTablesStatement showTables) { StatementVisitor.super.visit(showTables); } + @Override + public Void visit(ShowTableStatusStatement showTableStatus, S context) { + // reads the status of the tables of a database, but names no table + return null; + } + + @Override + public void visit(ShowTableStatusStatement showTableStatus) { + StatementVisitor.super.visit(showTableStatus); + } + + @Override + public Void visit(ShowCreateStatement showCreate, S context) { + ShowCreateStatement.ObjectType type = showCreate.getObjectType(); + if ((type == ShowCreateStatement.ObjectType.TABLE + || type == ShowCreateStatement.ObjectType.VIEW) && showCreate.getName() != null) { + visit(showCreate.getName(), context); + } + return null; + } + + @Override + public void visit(ShowCreateStatement showCreate) { + StatementVisitor.super.visit(showCreate); + } + @Override public Void visit(TSQLLeftJoin tsqlLeftJoin, S context) { visitBinaryExpression(tsqlLeftJoin); @@ -1631,6 +2529,26 @@ public Void visit(LambdaExpression lambdaExpression, S context) { return null; } + @Override + public Void visit(ListComprehension listComprehension, S context) { + listComprehension.getExpression().accept(this, context); + listComprehension.getList().accept(this, context); + if (listComprehension.getCondition() != null) { + listComprehension.getCondition().accept(this, context); + } + return null; + } + + @Override + public Void visit(ColumnsExpression columnsExpression, S context) { + for (Expression expression : columnsExpression.getAllExpressions()) { + if (expression != null) { + expression.accept(this, context); + } + } + return null; + } + @Override public Void visit(HighExpression highExpression, S context) { highExpression.getExpression().accept(this, context); @@ -1677,7 +2595,9 @@ public Void visit(VariableAssignment variableAssignment, S context) { @Override public Void visit(XMLSerializeExpr xmlSerializeExpr, S context) { - + for (Expression expression : xmlSerializeExpr.getExpressions()) { + expression.accept(this, context); + } return null; } @@ -1695,6 +2615,9 @@ public void visit(CreateSynonym createSynonym) { @Override public Void visit(TimezoneExpression timezoneExpression, S context) { timezoneExpression.getLeftExpression().accept(this, context); + for (Expression timezone : timezoneExpression.getTimezoneExpressions()) { + visitExpression(timezone, context); + } return null; } @@ -1736,11 +2659,21 @@ public Void visit(JsonAggregateFunction expression, S context) { if (expr != null) { expr.accept(this, context); } + if (expression.getKey() instanceof Expression) { + ((Expression) expression.getKey()).accept(this, context); + } + if (expression.getValue() instanceof Expression) { + ((Expression) expression.getValue()).accept(this, context); + } + visitOrderBy(expression.getExpressionOrderByElements(), context); expr = expression.getFilterExpression(); if (expr != null) { expr.accept(this, context); } + visitExpressions(expression.getPartitionExpressionList(), context); + visitOrderBy(expression.getOrderByElements(), context); + visitWindowElement(expression.getWindowElement(), context); return null; } @@ -1825,6 +2758,7 @@ public Void visit(KeyExpression keyExpression, S context) { @Override public Void visit(IfElseStatement ifElseStatement, S context) { + ifElseStatement.getCondition().accept(this, context); ifElseStatement.getIfStatement().accept(this, context); if (ifElseStatement.getElseStatement() != null) { ifElseStatement.getElseStatement().accept(this, context); @@ -1837,6 +2771,11 @@ public void visit(IfElseStatement ifElseStatement) { StatementVisitor.super.visit(ifElseStatement); } + @Override + public Void visit(AliasedExpression expression, S context) { + return expression.getExpression().accept(this, context); + } + @Override public Void visit(OracleNamedFunctionParameter oracleNamedFunctionParameter, S context) { oracleNamedFunctionParameter.getExpression().accept(this, context); @@ -1865,76 +2804,240 @@ public void visit(RenameTableStatement renameTableStatement) { } @Override - public Void visit(PurgeStatement purgeStatement, S context) { - if (purgeStatement.getPurgeObjectType() == PurgeObjectType.TABLE) { - ((Table) purgeStatement.getObject()).accept(this, context); - } + public Void visit(PragmaStatement pragmaStatement, S context) { + // no tables involved in this statement return null; } @Override - public void visit(PurgeStatement purgeStatement) { - StatementVisitor.super.visit(purgeStatement); - } - - @Override - public Void visit(AlterSystemStatement alterSystemStatement, S context) { - // no tables involved in this statement + public Void visit(AssertStatement assertStatement, S context) { + assertStatement.getExpression().accept(this, context); return null; } @Override - public void visit(AlterSystemStatement alterSystemStatement) { - StatementVisitor.super.visit(alterSystemStatement); + public void visit(PragmaStatement pragmaStatement) { + StatementVisitor.super.visit(pragmaStatement); } @Override - public Void visit(UnsupportedStatement unsupportedStatement, S context) { + public Void visit(ExtensionStatement extensionStatement, S context) { // no tables involved in this statement return null; } @Override - public void visit(UnsupportedStatement unsupportedStatement) { - StatementVisitor.super.visit(unsupportedStatement); + public void visit(AssertStatement assertStatement) { + StatementVisitor.super.visit(assertStatement); } @Override - public Void visit(GeometryDistance geometryDistance, S context) { - visitBinaryExpression(geometryDistance); + public Void visit(ExportDataStatement exportDataStatement, S context) { + exportDataStatement.getSelect().accept((SelectVisitor) this, context); return null; } @Override - public Void visit(Import imprt, S context) { - throwUnsupported(imprt); + public void visit(ExtensionStatement extensionStatement) { + StatementVisitor.super.visit(extensionStatement); + } + + @Override + public Void visit(AttachStatement attachStatement, S context) { + // no tables involved in this statement return null; } @Override - public void visit(Import imprt) { - StatementVisitor.super.visit(imprt); + public void visit(ExportDataStatement exportDataStatement) { + StatementVisitor.super.visit(exportDataStatement); } @Override - public Void visit(Export export, S context) { - throwUnsupported(export); + public Void visit(LoadDataStatement loadDataStatement, S context) { + loadDataStatement.getTable().accept(this, context); return null; } @Override - public void visit(Export export) { - StatementVisitor.super.visit(export); + public void visit(AttachStatement attachStatement) { + StatementVisitor.super.visit(attachStatement); } @Override - public Void visit(LockStatement lock, S context) { - lock.getTable().accept(this); + public Void visit(DetachStatement detachStatement, S context) { + // no tables involved in this statement return null; } @Override - public void visit(LockStatement lock) { + public void visit(DetachStatement detachStatement) { + StatementVisitor.super.visit(detachStatement); + } + + @Override + public Void visit(ConnectStatement connectStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(ConnectStatement connectStatement) { + StatementVisitor.super.visit(connectStatement); + } + + @Override + public Void visit(DisconnectStatement disconnectStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(DisconnectStatement disconnectStatement) { + StatementVisitor.super.visit(disconnectStatement); + } + + @Override + public Void visit(PrepareStatement prepareStatement, S context) { + prepareStatement.getStatement().accept(this, context); + return null; + } + + @Override + public void visit(PrepareStatement prepareStatement) { + StatementVisitor.super.visit(prepareStatement); + } + + @Override + public Void visit(DeallocateStatement deallocateStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(DeallocateStatement deallocateStatement) { + StatementVisitor.super.visit(deallocateStatement); + } + + @Override + public Void visit(CopyStatement copyStatement, S context) { + if (copyStatement.getSelect() != null) { + copyStatement.getSelect().accept((SelectVisitor) this, context); + } else if (copyStatement.getTable() != null) { + copyStatement.getTable().accept(this, context); + } + return null; + } + + @Override + public void visit(CopyStatement copyStatement) { + StatementVisitor.super.visit(copyStatement); + } + + @Override + public Void visit(CreateMacro createMacro, S context) { + for (CreateMacro.Overload overload : createMacro.getOverloads()) { + if (overload.getSelect() != null) { + overload.getSelect().accept((SelectVisitor) this, context); + } + } + return null; + } + + @Override + public void visit(CreateMacro createMacro) { + StatementVisitor.super.visit(createMacro); + } + + @Override + public Void visit(CreateExtensionRepository createExtensionRepository, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(CreateExtensionRepository createExtensionRepository) { + StatementVisitor.super.visit(createExtensionRepository); + } + + @Override + public void visit(LoadDataStatement loadDataStatement) { + StatementVisitor.super.visit(loadDataStatement); + } + + @Override + public Void visit(PurgeStatement purgeStatement, S context) { + if (purgeStatement.getPurgeObjectType() == PurgeObjectType.TABLE) { + ((Table) purgeStatement.getObject()).accept(this, context); + } + return null; + } + + @Override + public void visit(PurgeStatement purgeStatement) { + StatementVisitor.super.visit(purgeStatement); + } + + @Override + public Void visit(AlterSystemStatement alterSystemStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(AlterSystemStatement alterSystemStatement) { + StatementVisitor.super.visit(alterSystemStatement); + } + + @Override + public Void visit(UnsupportedStatement unsupportedStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(UnsupportedStatement unsupportedStatement) { + StatementVisitor.super.visit(unsupportedStatement); + } + + @Override + public Void visit(GeometryDistance geometryDistance, S context) { + visitBinaryExpression(geometryDistance); + return null; + } + + @Override + public Void visit(Import imprt, S context) { + throwUnsupported(imprt); + return null; + } + + @Override + public void visit(Import imprt) { + StatementVisitor.super.visit(imprt); + } + + @Override + public Void visit(Export export, S context) { + throwUnsupported(export); + return null; + } + + @Override + public void visit(Export export) { + StatementVisitor.super.visit(export); + } + + @Override + public Void visit(LockStatement lock, S context) { + for (LockStatement.Target target : lock.getTargets()) { + target.getTable().accept(this, context); + } + return null; + } + + @Override + public void visit(LockStatement lock) { StatementVisitor.super.visit(lock); } @@ -1961,4 +3064,281 @@ public Void visit(CreatePolicy createPolicy, S context) { public void visit(CreatePolicy createPolicy) { StatementVisitor.super.visit(createPolicy); } + + @Override + public Void visit(CreateRole statement, S context) { + return null; + } + + @Override + public Void visit(AlterSchema statement, S context) { + // Schema and owner names do not refer to tables. + return null; + } + + @Override + public Void visit(AlterRole statement, S context) { + return null; + } + + @Override + public Void visit(Revoke statement, S context) { + statement.getClause().visit(t -> t.accept(this, context), e -> e.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterDefaultPrivileges statement, S context) { + return null; + } + + @Override + public Void visit(CreateType statement, S context) { + + return null; + } + + @Override + public Void visit(AlterType statement, S context) { + + return null; + } + + @Override + public Void visit(CreateDomain statement, S context) { + if (statement.getDefaultExpression() != null) { + statement.getDefaultExpression().accept(this, context); + } + statement.getConstraints().forEach(constraint -> { + if (constraint.getExpression() != null) { + constraint.getExpression().accept(this, context); + } + }); + return null; + } + + @Override + public Void visit(AlterDomain statement, S context) { + if (statement.getDefaultExpression() != null) { + statement.getDefaultExpression().accept(this, context); + } + if (statement.getConstraint() != null + && statement.getConstraint().getExpression() != null) { + statement.getConstraint().getExpression().accept(this, context); + } + return null; + } + + @Override + public Void visit(CreateExtension statement, S context) { + + return null; + } + + @Override + public Void visit(AlterExtension statement, S context) { + if (statement.getMember() != null && statement.getMember().isTable()) { + visit(new Table(statement.getMember().getName()), context); + } + return null; + } + + @Override + public Void visit(CreatePublication statement, S context) { + statement.getTargets().forEach(target -> target.visit( + table -> table.accept(this, context), + expression -> expression.accept(this, context))); + return null; + } + + @Override + public Void visit(AlterPublication statement, S context) { + statement.getTargets().forEach(target -> target.visit( + table -> table.accept(this, context), + expression -> expression.accept(this, context))); + return null; + } + + @Override + public Void visit(CreateSubscription statement, S context) { + + return null; + } + + @Override + public Void visit(AlterSubscription statement, S context) { + + return null; + } + + @Override + public Void visit(OracleBlock block, S context) { + block.visitChildren(expression -> expression.accept(this, context), + statement -> statement.accept(this, context)); + return null; + } + + @Override + public Void visit(OracleAssignment assignment, S context) { + assignment.getValue().accept(this, context); + return null; + } + + @Override + public Void visit(OracleNullStatement statement, S context) { + return null; + } + + @Override + public Void visit(AlterRelation statement, S context) { + if (statement.getObjectType() != AlterRelation.ObjectType.INDEX) { + visit(statement.getRelation(), context); + } + statement.getActions().forEach(action -> { + action.visitExpressions(expression -> expression.accept(this, context)); + action.visitTables(table -> visit(table, context)); + }); + return null; + } + + @Override + public Void visit(AlterDatabase statement, S context) { + return null; + } + + @Override + public Void visit(AlterTablespaceMove statement, S context) { + return null; + } + + @Override + public Void visit(CreateTablespace statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterTablespace statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterPolicy statement, S context) { + visit(statement.getTable(), context); + statement.getOptions().visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(DropPolicy statement, S context) { + visit(statement.getTable(), context); + return null; + } + + @Override + public Void visit(CreateStatistics statement, S context) { + visit(statement.getTable(), context); + statement.getExpressions().accept(this, context); + return null; + } + + @Override + public Void visit(AlterStatistics statement, S context) { + return null; + } + + @Override + public Void visit(CreateForeignDataWrapper statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterForeignDataWrapper statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(CreateServer statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterServer statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(CreateUserMapping statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterUserMapping statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(DropUserMapping statement, S context) { + // User and server names do not refer to tables. + return null; + } + + @Override + public Void visit(CreateAccessMethod statement, S context) { + return null; + } + + @Override + public Void visit(CreateTextSearchConfiguration statement, S context) { + return null; + } + + @Override + public Void visit(AlterTextSearchConfiguration statement, S context) { + return null; + } + + @Override + public Void visit(CreateRule statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + statement.visitTables(table -> visit(table, context)); + statement.getActions().forEach(action -> action.accept(this, context)); + return null; + } + + @Override + public Void visit(NotifyStatement statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(CreateCollation statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterCollation statement, S context) { + return null; + } + + @Override + public Void visit(AlterRoutine statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterTrigger statement, S context) { + visit(statement.getTable(), context); + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/TemporalExpressionInfo.java b/src/main/java/net/sf/jsqlparser/util/TemporalExpressionInfo.java new file mode 100644 index 0000000000..a14ffc1422 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/TemporalExpressionInfo.java @@ -0,0 +1,190 @@ +/*- + * #%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.util; + +import java.math.BigInteger; +import java.util.Locale; +import java.util.Optional; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.TimeKeyExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.schema.Column; + +/** + * Read-only metadata for current date/time expressions in PostgreSQL and MySQL. The parser's + * existing TimeKeyExpression, Function and Column nodes are retained. Results are snapshots; + * inspect the expression again after editing its AST. This is not a database range validator. + */ +public final class TemporalExpressionInfo { + + public enum Kind { + CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP, LOCAL_TIME, LOCAL_TIMESTAMP + } + + private final Kind kind; + private final String name; + private final Integer precision; + private final boolean parentheses; + + private TemporalExpressionInfo(Kind kind, String name, Integer precision, boolean parentheses) { + this.kind = kind; + this.name = name; + this.precision = precision; + this.parentheses = parentheses; + } + + public Kind getKind() { + return kind; + } + + /** Returns the original keyword or function name, without argument parentheses. */ + public String getName() { + return name; + } + + /** + * Returns the explicitly requested fractional precision, or null when omitted. Zero is distinct + * from omission. Database defaults, range checks and precision clamping are not applied. + */ + public Integer getPrecision() { + return precision; + } + + public boolean hasParentheses() { + return parentheses; + } + + /** + * Recognizes standard current-time spellings and MySQL NOW/CURDATE/CURTIME aliases. MySQL + * LOCALTIME and LOCALTIMESTAMP normalize to CURRENT_TIMESTAMP; PostgreSQL retains their local + * time/time-stamp distinction. Quoted/qualified names, ordinary functions, unsupported dialects + * and expressions outside the supported call shapes return an empty result. + */ + public static Optional from(Expression source, Dialect dialect) { + if (dialect != Dialect.POSTGRESQL && dialect != Dialect.MYSQL) { + return Optional.empty(); + } + Expression expression = unwrap(source); + if (expression instanceof TimeKeyExpression) { + return fromKeyword(((TimeKeyExpression) expression).getStringValue(), dialect); + } + if (expression instanceof Column) { + Column column = (Column) expression; + if (column.getTable() != null && column.getTable().getName() != null) { + return Optional.empty(); + } + return create(column.getColumnName(), null, false, dialect); + } + if (expression instanceof Function) { + return fromFunction((Function) expression, dialect); + } + return Optional.empty(); + } + + private static Expression unwrap(Expression source) { + Expression expression = source; + while (expression instanceof ParenthesedExpressionList + && ((ParenthesedExpressionList) expression).size() == 1) { + expression = ((ParenthesedExpressionList) expression).get(0); + } + return expression; + } + + private static Optional fromKeyword(String name, Dialect dialect) { + boolean parentheses = name != null && name.endsWith("()"); + String keyword = parentheses ? name.substring(0, name.length() - 2) : name; + return create(keyword, null, parentheses, dialect); + } + + private static Optional fromFunction(Function function, + Dialect dialect) { + if (!isPlainCall(function)) { + return Optional.empty(); + } + if (function.getParameters() == null || function.getParameters().isEmpty()) { + return create(function.getName(), null, true, dialect); + } + if (function.getParameters().size() != 1 + || !(function.getParameters().get(0) instanceof LongValue)) { + return Optional.empty(); + } + BigInteger value = ((LongValue) function.getParameters().get(0)).getBigIntegerValue(); + if (value.signum() < 0 || value.bitLength() >= Integer.SIZE) { + return Optional.empty(); + } + return create(function.getName(), value.intValue(), true, dialect); + } + + private static Optional create(String name, Integer precision, + boolean parentheses, Dialect dialect) { + if (name == null) { + return Optional.empty(); + } + String normalized = name.toUpperCase(Locale.ROOT); + Kind kind = kind(normalized, dialect); + if (kind == null || !validCallShape(normalized, kind, precision, parentheses, dialect)) { + return Optional.empty(); + } + return Optional.of(new TemporalExpressionInfo(kind, name, precision, parentheses)); + } + + private static boolean isPlainCall(Function function) { + return function.getClass() == Function.class + && function.getMultipartName() != null && function.getMultipartName().size() == 1 + && !function.isEscaped() && !function.isAllColumns() && !function.isDistinct() + && !function.isUnique() && function.getNamedParameters() == null + && function.getChainedParameters() == null && function.getAttribute() == null + && function.getHavingClause() == null && function.getOrderByElements() == null + && function.getNullHandling() == null && function.getLimit() == null + && function.getKeep() == null && function.getOnOverflowTruncate() == null + && function.getExtraKeyword() == null && function.getKeywordArguments() == null; + } + + private static Kind kind(String name, Dialect dialect) { + switch (name) { + case "CURRENT_DATE": + return Kind.CURRENT_DATE; + case "CURRENT_TIME": + return Kind.CURRENT_TIME; + case "CURRENT_TIMESTAMP": + case "NOW": + return Kind.CURRENT_TIMESTAMP; + case "LOCALTIME": + return dialect == Dialect.POSTGRESQL ? Kind.LOCAL_TIME : Kind.CURRENT_TIMESTAMP; + case "LOCALTIMESTAMP": + return dialect == Dialect.POSTGRESQL ? Kind.LOCAL_TIMESTAMP + : Kind.CURRENT_TIMESTAMP; + case "CURDATE": + return dialect == Dialect.MYSQL ? Kind.CURRENT_DATE : null; + case "CURTIME": + return dialect == Dialect.MYSQL ? Kind.CURRENT_TIME : null; + default: + return null; + } + } + + private static boolean validCallShape(String name, Kind kind, Integer precision, + boolean parentheses, Dialect dialect) { + if (("NOW".equals(name) || "CURDATE".equals(name) || "CURTIME".equals(name)) + && !parentheses) { + return false; + } + if (kind == Kind.CURRENT_DATE && precision != null) { + return false; + } + if (dialect == Dialect.POSTGRESQL) { + return "NOW".equals(name) ? precision == null : parentheses == (precision != null); + } + return true; + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AbstractDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AbstractDeParser.java index 6458f79c9c..7217c6e1f2 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AbstractDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AbstractDeParser.java @@ -9,8 +9,10 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.ReturningClause; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.update.UpdateSet; +import net.sf.jsqlparser.statement.ForPortionClause; import java.util.List; @@ -26,6 +28,14 @@ protected AbstractDeParser(StringBuilder builder) { this.builder = builder; } + protected void deparseReturningClause(ReturningClause clause, + ExpressionVisitor visitor) { + if (clause != null) { + SelectDeParser selectItems = new SelectDeParser(visitor, builder); + clause.appendTo(builder, item -> item.accept(selectItems, null)); + } + } + public static void deparseUpdateSets(List updateSets, StringBuilder buffer, ExpressionVisitor visitor) { ExpressionListDeParser expressionListDeParser = @@ -43,6 +53,14 @@ public static void deparseUpdateSets(List updateSets, StringBuilder b } } + protected void deparseForPortionClause(ForPortionClause clause, + ExpressionVisitor visitor) { + if (clause != null) { + builder.append(' '); + clause.appendTo(builder, expression -> expression.accept(visitor, null)); + } + } + public StringBuilder getBuilder() { return builder; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java index bfc00e7ef3..5c09eb9bec 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -10,16 +10,104 @@ package net.sf.jsqlparser.util.deparser; import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterForeignDataOptions; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterExpressionPartition; +import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey; +import net.sf.jsqlparser.statement.alter.RelationAlterAction; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.create.table.IndexOptionSource; public class AlterDeParser extends AbstractDeParser { + private final ExpressionVisitor expressionVisitor; public AlterDeParser(StringBuilder buffer) { + this(buffer, new StatementDeParser(buffer).getExpressionDeParser()); + } + + public AlterDeParser(StringBuilder buffer, ExpressionVisitor expressionVisitor) { super(buffer); + this.expressionVisitor = expressionVisitor; } @Override public void deParse(Alter alter) { - builder.append(alter.toString()); + alter.appendTargetTo(builder); + boolean first = true; + for (AlterExpression action : alter.getAlterExpressions()) { + if (!first) { + builder.append(Alter.separatorBefore(action)); + } + deParseAction(action); + first = false; + } + } + + private void deParseAction(AlterExpression action) { + if (action instanceof AlterForeignDataOptions) { + ((AlterForeignDataOptions) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } + if (action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) { + ((net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } + if (action instanceof RelationAlterAction) { + ((RelationAlterAction) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } + if (action instanceof AlterExpressionPartition) { + ((AlterExpressionPartition) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } + if (action instanceof AlterExpressionPrimaryKey) { + ((AlterExpressionPrimaryKey) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } + if (action.hasActiveTableDefinition()) { + builder.append(action.getOperation()).append(' '); + new TableElementDeParser(builder, expressionVisitor).deParse( + action.getConstraint() != null ? action.getConstraint() : action.getIndex()); + if (action.getConstraints() != null && !action.getConstraints().isEmpty()) { + builder.append(' ') + .append(PlainSelect.getStringList(action.getConstraints(), false, false)); + } + if (action.getUseEqual()) { + builder.append('='); + } + deParseTail(action); + return; + } + if (!action.getColumnDefaults().isEmpty()) { + action.appendColumnActionTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } + if (action.hasActiveColumnDefinitions()) { + action.appendColumnDefinitionsTo(builder, + expression -> expression.accept(expressionVisitor, null)); + } else { + builder.append(action); + } + } + + private void deParseTail(AlterExpression action) { + if (action.getParameters() != null && !action.getParameters().isEmpty()) { + builder.append(' ') + .append(PlainSelect.getStringList(action.getParameters(), false, false)); + } + IndexOptionSource options = action.getConstraint() instanceof IndexOptionSource + ? (IndexOptionSource) action.getConstraint() + : action.getIndex(); + if (options != null && options.getIndexOptions().getCommentText() != null) { + builder.append(" COMMENT ").append(options.getIndexOptions().getCommentText()); + } } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterSequenceDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterSequenceDeParser.java index b9c743bdce..f857a5caf1 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterSequenceDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterSequenceDeParser.java @@ -26,7 +26,6 @@ public AlterSequenceDeParser(StringBuilder buffer) { @Override public void deParse(AlterSequence statement) { - builder.append("ALTER SEQUENCE "); - builder.append(statement.getSequence()); + statement.appendTo(builder); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterViewDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterViewDeParser.java index 3c1c0bbaa2..514f6f6a07 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterViewDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterViewDeParser.java @@ -37,6 +37,7 @@ public void deParse(AlterView alterView) { } else { builder.append("ALTER "); } + alterView.appendMySqlOptionsTo(builder); builder.append("VIEW ").append(alterView.getView().getFullyQualifiedName()); if (alterView.getColumnNames() != null) { builder.append(PlainSelect.getStringList(alterView.getColumnNames(), true, true)); @@ -44,6 +45,7 @@ public void deParse(AlterView alterView) { builder.append(" AS "); alterView.getSelect().accept(selectVisitor, null); + alterView.appendCheckOptionTo(builder); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java index cc5d71ba18..94ac42e7ef 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java @@ -9,61 +9,28 @@ */ package net.sf.jsqlparser.util.deparser; -import static java.util.stream.Collectors.joining; - +import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.create.index.CreateIndex; -import net.sf.jsqlparser.statement.create.table.Index; public class CreateIndexDeParser extends AbstractDeParser { + private final ExpressionVisitor expressionVisitor; public CreateIndexDeParser(StringBuilder buffer) { + this(buffer, null); + } + + public CreateIndexDeParser(StringBuilder buffer, + ExpressionVisitor expressionVisitor) { super(buffer); + this.expressionVisitor = expressionVisitor; } @Override public void deParse(CreateIndex createIndex) { - Index index = createIndex.getIndex(); - - builder.append("CREATE "); - - if (index.getType() != null) { - builder.append(index.getType()); - builder.append(" "); - } - - builder.append("INDEX "); - if (createIndex.isUsingIfNotExists()) { - builder.append("IF NOT EXISTS "); - } - builder.append(index.getName()); - - String using = index.getUsing(); - if (using != null && createIndex.isIndexTypeBeforeOn()) { - builder.append(" USING "); - builder.append(using); - } - - builder.append(" ON "); - builder.append(createIndex.getTable().getFullyQualifiedName()); - - if (using != null && !createIndex.isIndexTypeBeforeOn()) { - builder.append(" USING "); - builder.append(using); - } - - if (index.getColumnsNames() != null) { - builder.append(" ("); - builder.append(index.getColumnWithParams().stream() - .map(Index.ColumnParams::toString) - .collect(joining(", "))); - builder.append(")"); - } - - if (createIndex.getTailParameters() != null) { - for (String param : createIndex.getTailParameters()) { - builder.append(" ").append(param); - } + if (expressionVisitor == null) { + createIndex.appendTo(builder); + } else { + createIndex.appendTo(builder, expression -> expression.accept(expressionVisitor, null)); } } - } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java new file mode 100644 index 0000000000..4d604494ec --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java @@ -0,0 +1,36 @@ +/*- + * #%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.util.deparser; + +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.statement.create.policy.CreatePolicy; + +public class CreatePolicyDeParser extends AbstractDeParser { + + private final ExpressionVisitor expressionVisitor; + + public CreatePolicyDeParser(StringBuilder builder) { + super(builder); + ExpressionDeParser expressionDeParser = new ExpressionDeParser(); + expressionDeParser.setBuilder(builder); + this.expressionVisitor = expressionDeParser; + } + + public CreatePolicyDeParser(ExpressionVisitor expressionVisitor, + StringBuilder builder) { + super(builder); + this.expressionVisitor = expressionVisitor; + } + + @Override + public void deParse(CreatePolicy createPolicy) { + createPolicy.appendTo(builder, expression -> expression.accept(expressionVisitor, null)); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateSequenceDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateSequenceDeParser.java index d4f4750987..f2b1859dc2 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateSequenceDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateSequenceDeParser.java @@ -26,7 +26,6 @@ public CreateSequenceDeParser(StringBuilder buffer) { @Override public void deParse(CreateSequence statement) { - builder.append("CREATE SEQUENCE "); - builder.append(statement.getSequence()); + statement.appendTo(builder); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java index 1d9fc85f35..5348e4b9ab 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java @@ -10,20 +10,20 @@ package net.sf.jsqlparser.util.deparser; import java.util.Iterator; +import java.util.ArrayList; +import java.util.List; import net.sf.jsqlparser.schema.Table; -import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.create.table.CreateTable; -import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.TableElement; import net.sf.jsqlparser.statement.select.PlainSelect; -import net.sf.jsqlparser.statement.select.Select; public class CreateTableDeParser extends AbstractDeParser { private StatementDeParser statementDeParser; public CreateTableDeParser(StringBuilder buffer) { - super(buffer); + this(new StatementDeParser(buffer), buffer); } public CreateTableDeParser(StatementDeParser statementDeParser, StringBuilder buffer) { @@ -34,6 +34,8 @@ public CreateTableDeParser(StatementDeParser statementDeParser, StringBuilder bu @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public void deParse(CreateTable createTable) { + TableElementDeParser elements = + new TableElementDeParser(builder, statementDeParser.getExpressionDeParser()); builder.append("CREATE "); if (createTable.isOrReplace()) { builder.append("OR REPLACE "); @@ -52,6 +54,16 @@ public void deParse(CreateTable createTable) { builder.append("IF NOT EXISTS "); } builder.append(createTable.getTable().getFullyQualifiedName()); + if (createTable.getOfType() != null) { + builder.append(" OF ").append(createTable.getOfType()); + } + if (createTable.getPartitionOf() != null) { + builder.append(" PARTITION OF ") + .append(createTable.getPartitionOf().getFullyQualifiedName()); + } + if (createTable.getCloneTable() != null) { + builder.append(" CLONE ").append(createTable.getCloneTable()); + } if (createTable.getColumns() != null && !createTable.getColumns().isEmpty()) { builder.append(" ("); @@ -62,62 +74,65 @@ public void deParse(CreateTable createTable) { } builder.append(")"); } - if (createTable.getColumnDefinitions() != null) { - builder.append(" ("); - for (Iterator iter = - createTable.getColumnDefinitions().iterator(); iter.hasNext();) { - ColumnDefinition columnDefinition = iter.next(); - builder.append(columnDefinition.getColumnName()); - builder.append(" "); - builder.append(columnDefinition.getColDataType().toString()); - if (columnDefinition.getColumnSpecs() != null) { - for (String s : columnDefinition.getColumnSpecs()) { - builder.append(" "); - builder.append(s); - } - } - - if (iter.hasNext()) { - builder.append(", "); - } + List definitions = createTable.getTableElements(); + if (definitions == null) { + definitions = new ArrayList<>(); + if (createTable.getColumnDefinitions() != null) { + definitions.addAll(createTable.getColumnDefinitions()); + } + if (createTable.getTableConstraints() != null) { + definitions.addAll(createTable.getTableConstraints()); } - if (createTable.getIndexes() != null) { - for (Index index : createTable.getIndexes()) { + definitions.addAll(createTable.getIndexes()); + } + } + if (createTable.getTableElements() != null || !definitions.isEmpty()) { + builder.append(" ("); + for (Iterator iter = definitions.iterator(); iter.hasNext();) { + elements.deParse(iter.next()); + if (iter.hasNext()) { builder.append(", "); - builder.append(index.toString()); } } - builder.append(")"); } - params = PlainSelect.getStringList(createTable.getTableOptionsStrings(), false, false); - if (!"".equals(params)) { - builder.append(' ').append(params); + createTable.appendInheritanceTo(builder); + if (createTable.getPartitionBound() != null) { + builder.append(' '); + createTable.getPartitionBound().appendTo(builder, + expression -> expression.accept(statementDeParser.getExpressionDeParser(), + null)); + } + + if (createTable.getPartitioning() != null && createTable.isTableOptionsAfterPartition()) { + builder.append(' '); + createTable.getPartitioning().appendTo(builder, + expression -> expression.accept(statementDeParser.getExpressionDeParser(), + null)); + } + createTable.appendTableOptionsTo(builder, + expression -> expression.accept(statementDeParser.getExpressionDeParser(), null)); + if (createTable.getPartitioning() != null && !createTable.isTableOptionsAfterPartition()) { + builder.append(' '); + createTable.getPartitioning().appendTo(builder, + expression -> expression.accept(statementDeParser.getExpressionDeParser(), + null)); } if (createTable.getRowMovement() != null) { builder.append(' ').append(createTable.getRowMovement().getMode().toString()) .append(" ROW MOVEMENT"); } - if (createTable.getSelect() != null) { - builder.append(" AS "); - if (createTable.isSelectParenthesis()) { - builder.append("("); - } - Select sel = createTable.getSelect(); - sel.accept(this.statementDeParser, null); - if (createTable.isSelectParenthesis()) { - builder.append(")"); - } - } - if (createTable.getLikeTable() != null) { + createTable.appendQueryTo(builder, select -> select.accept(this.statementDeParser, null), + execute -> execute.accept(this.statementDeParser, null)); + if (createTable.getTrailingLikeTable() != null) { builder.append(" LIKE "); if (createTable.isSelectParenthesis()) { builder.append("("); } - Table table = createTable.getLikeTable(); + Table table = createTable.getTrailingLikeTable(); builder.append(table.getFullyQualifiedName()); if (createTable.isSelectParenthesis()) { builder.append(")"); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java index 196b4d4d9d..e2c34e5318 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java @@ -9,7 +9,6 @@ */ package net.sf.jsqlparser.util.deparser; -import net.sf.jsqlparser.statement.create.view.AutoRefreshOption; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.create.view.TemporaryOption; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -37,10 +36,12 @@ public CreateViewDeParser(StringBuilder buffer, SelectVisitor sel @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public void deParse(CreateView createView) { + createView.validateOptions(); builder.append("CREATE "); if (createView.isOrReplace()) { builder.append("OR REPLACE "); } + createView.appendMySqlOptionsTo(builder); switch (createView.getForce()) { case FORCE: builder.append("FORCE "); @@ -59,16 +60,21 @@ public void deParse(CreateView createView) { if (createView.getTemporary() != TemporaryOption.NONE) { builder.append(createView.getTemporary().name()).append(" "); } + if (createView.isRecursive()) { + builder.append("RECURSIVE "); + } if (createView.isMaterialized()) { builder.append("MATERIALIZED "); } - builder.append("VIEW ").append(createView.getView().getFullyQualifiedName()); - if (createView.isIfNotExists()) { - builder.append(" IF NOT EXISTS"); + builder.append("VIEW "); + if (createView.isIfNotExists() && !createView.isIfNotExistsAfterViewName()) { + builder.append("IF NOT EXISTS "); } - if (createView.getAutoRefresh() != AutoRefreshOption.NONE) { - builder.append(" AUTO REFRESH ").append(createView.getAutoRefresh().name()); + builder.append(createView.getView().getFullyQualifiedName()); + if (createView.isIfNotExists() && createView.isIfNotExistsAfterViewName()) { + builder.append(" IF NOT EXISTS"); } + createView.appendMaterializationOptionsTo(builder); if (createView.getColumnNames() != null) { builder.append("("); builder.append(createView.getColumnNames()); @@ -78,6 +84,7 @@ public void deParse(CreateView createView) { builder.append( PlainSelect.getStringList(createView.getViewCommentOptions(), false, false)); } + createView.appendParametersTo(builder); builder.append(" AS "); Select select = createView.getSelect(); @@ -85,6 +92,7 @@ public void deParse(CreateView createView) { if (createView.isWithReadOnly()) { builder.append(" WITH READ ONLY"); } + createView.appendOptionsAfterQueryTo(builder); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java index 2ab59ea148..23b3f8f601 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java @@ -13,10 +13,6 @@ import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.delete.Delete; -import net.sf.jsqlparser.statement.select.Join; -import net.sf.jsqlparser.statement.select.WithItem; - -import java.util.Iterator; import static java.util.stream.Collectors.joining; @@ -25,31 +21,30 @@ public class DeleteDeParser extends AbstractDeParser { private ExpressionVisitor expressionVisitor = new ExpressionVisitorAdapter(); + private SelectDeParser selectDeParser; + public DeleteDeParser() { super(new StringBuilder()); } public DeleteDeParser(ExpressionVisitor expressionVisitor, StringBuilder buffer) { + this(expressionVisitor, null, buffer); + } + + public DeleteDeParser(ExpressionVisitor expressionVisitor, + SelectDeParser selectDeParser, StringBuilder buffer) { super(buffer); this.expressionVisitor = expressionVisitor; + this.selectDeParser = selectDeParser; } @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public void deParse(Delete delete) { - if (delete.getWithItemsList() != null && !delete.getWithItemsList().isEmpty()) { - builder.append("WITH "); - for (Iterator> iter = delete.getWithItemsList().iterator(); iter - .hasNext();) { - WithItem withItem = iter.next(); - builder.append(withItem); - if (iter.hasNext()) { - builder.append(","); - } - builder.append(" "); - } - } + DmlDeParserSupport sources = + new DmlDeParserSupport(expressionVisitor, selectDeParser, builder); + sources.deparseWithItems(delete.getWithItemsList(), null); builder.append("DELETE"); if (delete.getOracleHint() != null) { builder.append(delete.getOracleHint()).append(" "); @@ -77,21 +72,10 @@ public void deParse(Delete delete) { builder.append(" FROM"); } builder.append(" ").append(delete.getTable().toString()); + deparseForPortionClause(delete.getForPortionClause(), expressionVisitor); - if (delete.getUsingFromItemList() != null && !delete.getUsingFromItemList().isEmpty()) { - builder.append(" USING").append( - delete.getUsingFromItemList().stream().map(Object::toString) - .collect(joining(", ", " ", ""))); - } - if (delete.getJoins() != null) { - for (Join join : delete.getJoins()) { - if (join.isSimple()) { - builder.append(", ").append(join); - } else { - builder.append(" ").append(join); - } - } - } + sources.deparseUsing(delete.getUsingFromItemList()); + sources.deparseJoins(delete.getJoins()); deparseWhereClause(delete); @@ -105,10 +89,12 @@ public void deParse(Delete delete) { new LimitDeparser(expressionVisitor, builder).deParse(delete.getLimit()); } - if (delete.getReturningClause() != null) { - delete.getReturningClause().appendTo(builder); + if (delete.getOption() != null) { + builder.append(delete.getOption()); } + deparseReturningClause(delete.getReturningClause(), expressionVisitor); + } protected void deparseWhereClause(Delete delete) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java b/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java new file mode 100644 index 0000000000..13138db4d4 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java @@ -0,0 +1,144 @@ +/*- + * #%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.util.deparser; + +import java.util.List; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.delete.ParenthesedDelete; +import net.sf.jsqlparser.statement.insert.ParenthesedInsert; +import net.sf.jsqlparser.statement.update.ParenthesedUpdate; +import net.sf.jsqlparser.statement.select.*; + +/** Shared source rendering for DML and the statements nested in WITH items. */ +final class DmlDeParserSupport { + private final ExpressionVisitor expressions; + private final SelectDeParser selects; + private final StringBuilder builder; + + DmlDeParserSupport(ExpressionVisitor expressions, SelectDeParser selects, + StringBuilder builder) { + this.expressions = expressions; + this.builder = builder; + SelectDeParser selected = selects; + if (selected == null && expressions instanceof ExpressionDeParser) { + SelectVisitor visitor = + ((ExpressionDeParser) expressions).getSelectVisitor(); + if (visitor instanceof SelectDeParser) { + selected = (SelectDeParser) visitor; + } + } + this.selects = selected == null ? new SelectDeParser(expressions, builder) : selected; + this.selects.setBuilder(builder); + this.selects.setExpressionVisitor(expressions); + } + + void deparseWithItems(List> items, S context) { + if (items != null && !items.isEmpty()) { + builder.append("WITH "); + for (int i = 0; i < items.size(); i++) { + if (i > 0) { + builder.append(", "); + } + selects.visit(items.get(i), context); + } + builder.append(' '); + } + } + + void deparseJoins(List joins) { + if (joins != null) { + for (Join join : joins) { + selects.deparseJoin(join); + } + } + } + + void deparseFrom(FromItem item, List joins) { + if (item != null) { + builder.append(" FROM "); + item.accept(selects, null); + deparseJoins(joins); + } + } + + void deparseUsing(List items) { + if (items != null && !items.isEmpty()) { + builder.append(" USING "); + for (int i = 0; i < items.size(); i++) { + if (i > 0) { + builder.append(", "); + } + items.get(i).accept(selects, null); + } + } + } + + StringBuilder deparseWithItem(WithItem item, S context) { + if (item.getWithFunctionDeclaration() != null) { + return builder.append(item.getWithFunctionDeclaration()); + } + if (item.getExpression() != null) { + item.getExpression().accept(expressions, context); + if (item.getAlias() != null) { + builder.append(" AS ").append(item.getAlias().getName()); + } + return item.appendRecursiveClausesTo(builder, + expression -> expression.accept(expressions, context)); + } + if (item.isRecursive()) { + builder.append("RECURSIVE "); + } + builder.append(item.getAlias().getName()); + if (item.getWithItemList() != null) { + builder.append(' ') + .append(PlainSelect.getStringList(item.getWithItemList(), true, true)); + } + if (item.getUsingKeyExpressions() != null) { + builder.append(" USING KEY (").append(item.getUsingKeyExpressions()).append(")"); + } + builder.append(" AS "); + if (item.isMaterialized()) { + builder.append(item.isUsingNot() ? "NOT MATERIALIZED " : "MATERIALIZED "); + } + item.getParenthesedStatement().accept(new StatementVisitorAdapter() { + @Override + public StringBuilder visit(Select select, T nestedContext) { + return select.accept((SelectVisitor) selects, nestedContext); + } + + @Override + public StringBuilder visit(ParenthesedInsert insert, T nestedContext) { + deparseWithItems(insert.getWithItemsList(), nestedContext); + builder.append('('); + new InsertDeParser(expressions, selects, builder).deParse(insert.getInsert()); + return builder.append(')'); + } + + @Override + public StringBuilder visit(ParenthesedUpdate update, T nestedContext) { + deparseWithItems(update.getWithItemsList(), nestedContext); + builder.append('('); + new UpdateDeParser(expressions, selects, builder).deParse(update.getUpdate()); + return builder.append(')'); + } + + @Override + public StringBuilder visit(ParenthesedDelete delete, T nestedContext) { + deparseWithItems(delete.getWithItemsList(), nestedContext); + builder.append('('); + new DeleteDeParser(expressions, selects, builder).deParse(delete.getDelete()); + return builder.append(')'); + } + }, context); + return item.appendRecursiveClausesTo(builder, + expression -> expression.accept(expressions, context)); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/DropDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/DropDeParser.java index 64c39b5051..569abd5cd8 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/DropDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/DropDeParser.java @@ -9,39 +9,24 @@ */ package net.sf.jsqlparser.util.deparser; +import java.util.function.Consumer; +import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.drop.Drop; -import net.sf.jsqlparser.statement.select.PlainSelect; public class DropDeParser extends AbstractDeParser { + private final Consumer
tablePrinter; public DropDeParser(StringBuilder buffer) { + this(buffer, buffer::append); + } + + public DropDeParser(StringBuilder buffer, Consumer
tablePrinter) { super(buffer); + this.tablePrinter = tablePrinter; } @Override public void deParse(Drop drop) { - builder.append("DROP "); - if (drop.isUsingTemporary()) { - builder.append("TEMPORARY "); - } - if (drop.isMaterialized()) { - builder.append("MATERIALIZED "); - } - builder.append(drop.getType()); - if (drop.isIfExists()) { - builder.append(" IF EXISTS"); - } - - builder.append(" ").append(drop.getName()); - - if (drop.getType().equals("FUNCTION")) { - builder.append(Drop.formatFuncParams(drop.getParamsByType("FUNCTION"))); - } - - if (drop.getParameters() != null && !drop.getParameters().isEmpty()) { - builder.append(" ") - .append(PlainSelect.getStringList(drop.getParameters(), false, false)); - } + drop.appendTo(builder, tablePrinter); } - } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExecuteDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExecuteDeParser.java index 4034bd152d..a4292b3802 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExecuteDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExecuteDeParser.java @@ -9,9 +9,7 @@ */ package net.sf.jsqlparser.util.deparser; -import java.util.List; -import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.execute.Execute; @@ -27,24 +25,7 @@ public ExecuteDeParser(ExpressionVisitor expressionVisitor, @Override public void deParse(Execute execute) { - builder.append(execute.getExecType().name()).append(" ").append(execute.getName()); - if (execute.isParenthesis()) { - builder.append(" ("); - } else if (execute.getExprList() != null) { - builder.append(" "); - } - if (execute.getExprList() != null) { - List expressions = execute.getExprList().getExpressions(); - for (int i = 0; i < expressions.size(); i++) { - if (i > 0) { - builder.append(", "); - } - expressions.get(i).accept(expressionVisitor, null); - } - } - if (execute.isParenthesis()) { - builder.append(")"); - } + execute.appendTo(builder, expression -> expression.accept(expressionVisitor, null)); } public ExpressionVisitor getExpressionVisitor() { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index e50ccad6a6..8183fd13aa 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -14,6 +14,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import net.sf.jsqlparser.expression.AliasedExpression; import net.sf.jsqlparser.expression.AllValue; import net.sf.jsqlparser.expression.AnalyticExpression; import net.sf.jsqlparser.expression.AnalyticType; @@ -25,6 +26,8 @@ import net.sf.jsqlparser.expression.CaseExpression; import net.sf.jsqlparser.expression.CastExpression; import net.sf.jsqlparser.expression.CollateExpression; +import net.sf.jsqlparser.expression.ColumnsExpression; +import net.sf.jsqlparser.expression.ColumnsTransformer; import net.sf.jsqlparser.expression.ConnectByPriorOperator; import net.sf.jsqlparser.expression.ConnectByRootOperator; import net.sf.jsqlparser.expression.DateTimeLiteralExpression; @@ -45,12 +48,14 @@ import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.JsonTableFunction; -import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.KeyExpression; import net.sf.jsqlparser.expression.LambdaExpression; +import net.sf.jsqlparser.expression.ListComprehension; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.LowExpression; +import net.sf.jsqlparser.expression.MapExpression; +import net.sf.jsqlparser.expression.MethodCallExpression; import net.sf.jsqlparser.expression.MySQLGroupConcat; import net.sf.jsqlparser.expression.NextValExpression; import net.sf.jsqlparser.expression.NotExpression; @@ -64,9 +69,11 @@ import net.sf.jsqlparser.expression.RangeExpression; import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.RowGetExpression; +import net.sf.jsqlparser.expression.RowPatternFunction; import net.sf.jsqlparser.expression.SignedExpression; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.StructType; +import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.TimeKeyExpression; import net.sf.jsqlparser.expression.TimeValue; import net.sf.jsqlparser.expression.TimestampValue; @@ -78,6 +85,7 @@ import net.sf.jsqlparser.expression.WhenClause; import net.sf.jsqlparser.expression.WindowElement; import net.sf.jsqlparser.expression.XMLSerializeExpr; +import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -86,6 +94,7 @@ import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor; import net.sf.jsqlparser.expression.operators.arithmetic.Concat; import net.sf.jsqlparser.expression.operators.arithmetic.Division; +import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation; import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision; import net.sf.jsqlparser.expression.operators.arithmetic.Modulo; import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication; @@ -108,8 +117,10 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; +import net.sf.jsqlparser.expression.operators.relational.IsJsonExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; import net.sf.jsqlparser.expression.operators.relational.IsUnknownExpression; import net.sf.jsqlparser.expression.operators.relational.JsonOperator; @@ -139,6 +150,7 @@ import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectItem; import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.select.Values; import net.sf.jsqlparser.statement.select.WithItem; @SuppressWarnings({"PMD.CyclomaticComplexity"}) @@ -165,21 +177,28 @@ public ExpressionDeParser(SelectVisitor selectVisitor, StringBuil this.orderByDeParser = orderByDeParser; } + @Override + public StringBuilder visit(AliasedExpression expression, S context) { + return expression.appendTo(builder, value -> value.accept(this, context)); + } + @Override public StringBuilder visit(Addition addition, S context) { - deparse(addition, " + ", null); + deparse(addition, " + ", context); return builder; } @Override public StringBuilder visit(AndExpression andExpression, S context) { - deparse(andExpression, andExpression.isUseOperator() ? " && " : " AND ", - null); + deparse(andExpression, andExpression.isUseOperator() ? " && " : " AND ", context); return builder; } @Override public StringBuilder visit(Between between, S context) { + if (between.getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } between.getLeftExpression().accept(this, context); if (between.isNot()) { builder.append(" NOT"); @@ -208,19 +227,25 @@ public StringBuilder visit(OverlapsCondition overlapsCondition, S context) { @Override public StringBuilder visit(EqualsTo equalsTo, S context) { - deparse(equalsTo, " = ", null); + deparse(equalsTo, " " + equalsTo.getStringExpression() + " ", context); return builder; } @Override public StringBuilder visit(Division division, S context) { - deparse(division, " / ", null); + deparse(division, " / ", context); return builder; } @Override public StringBuilder visit(IntegerDivision division, S context) { - deparse(division, " DIV ", null); + deparse(division, " DIV ", context); + return builder; + } + + @Override + public StringBuilder visit(Exponentiation exponentiation, S context) { + deparse(exponentiation, " ** ", context); return builder; } @@ -249,13 +274,13 @@ public StringBuilder visit(NotExpression notExpr, S context) { @Override public StringBuilder visit(BitwiseRightShift expr, S context) { - deparse(expr, " >> ", null); + deparse(expr, " >> ", context); return builder; } @Override public StringBuilder visit(BitwiseLeftShift expr, S context) { - deparse(expr, " << ", null); + deparse(expr, " << ", context); return builder; } @@ -265,11 +290,17 @@ public StringBuilder deparse( // if (expression.isNot()) { // buffer.append(NOT); // } + if (expression.getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } expression.getLeftExpression().accept(this, context); if (expression.getOldOracleJoinSyntax() == EqualsTo.ORACLE_JOIN_RIGHT) { builder.append("(+)"); } builder.append(operator); + if (expression.getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_END) { + builder.append("PRIOR "); + } expression.getRightExpression().accept(this, context); if (expression.getOldOracleJoinSyntax() == EqualsTo.ORACLE_JOIN_LEFT) { builder.append("(+)"); @@ -280,13 +311,13 @@ public StringBuilder deparse( @Override public StringBuilder visit(GreaterThan greaterThan, S context) { - deparse(greaterThan, " > ", null); + deparse(greaterThan, " > ", context); return builder; } @Override public StringBuilder visit(GreaterThanEquals greaterThanEquals, S context) { - deparse(greaterThanEquals, " >= ", null); + deparse(greaterThanEquals, " >= ", context); return builder; } @@ -342,6 +373,10 @@ public void visit(BitwiseLeftShift expr) { @Override public StringBuilder visit(InExpression inExpression, S context) { + if (inExpression + .getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } inExpression.getLeftExpression().accept(this, context); if (inExpression .getOldOracleJoinSyntax() == SupportsOldOracleJoinSyntax.ORACLE_JOIN_RIGHT) { @@ -398,12 +433,21 @@ public StringBuilder visit(FullTextSearch fullTextSearch, S context) { @Override public StringBuilder visit(SignedExpression signedExpression, S context) { builder.append(signedExpression.getSign()); + int operandStart = builder.length(); signedExpression.getExpression().accept(this, context); + if (builder.length() > operandStart + && signedExpression.needsSpaceBefore(builder.charAt(operandStart))) { + builder.insert(operandStart, ' '); + } return builder; } @Override public StringBuilder visit(IsNullExpression isNullExpression, S context) { + if (isNullExpression + .getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } isNullExpression.getLeftExpression().accept(this, context); if (isNullExpression.isUseNotNull()) { builder.append(" NOTNULL"); @@ -425,6 +469,10 @@ public StringBuilder visit(IsNullExpression isNullExpression, S context) { @Override public StringBuilder visit(IsBooleanExpression isBooleanExpression, S context) { + if (isBooleanExpression + .getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } isBooleanExpression.getLeftExpression().accept(this, context); if (isBooleanExpression.isTrue()) { if (isBooleanExpression.isNot()) { @@ -442,8 +490,17 @@ public StringBuilder visit(IsBooleanExpression isBooleanExpression, S contex return builder; } + @Override + public StringBuilder visit(IsJsonExpression expression, S context) { + return expression.appendTo(builder, value -> value.accept(this, context)); + } + @Override public StringBuilder visit(IsUnknownExpression isUnknownExpression, S context) { + if (isUnknownExpression + .getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } isUnknownExpression.getLeftExpression().accept(this, context); if (isUnknownExpression.isNot()) { builder.append(" IS NOT UNKNOWN"); @@ -469,12 +526,20 @@ public StringBuilder visit(LikeExpression likeExpression, S context) { ? " SIMILAR TO" : likeExpression.getLikeKeyWord().toString(); + if (likeExpression + .getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } likeExpression.getLeftExpression().accept(this, context); builder.append(" "); - if (likeExpression.isNot()) { - builder.append("NOT "); + if (likeExpression.getLikeOperator() != null) { + builder.append(likeExpression.getLikeOperator()).append(" "); + } else { + if (likeExpression.isNot()) { + builder.append("NOT "); + } + builder.append(keywordStr).append(" "); } - builder.append(keywordStr).append(" "); if (likeExpression.isUseBinary()) { builder.append("BINARY "); } @@ -501,6 +566,10 @@ public StringBuilder visit(ExistsExpression existsExpression, S context) { @Override public StringBuilder visit(MemberOfExpression memberOfExpression, S context) { + if (memberOfExpression + .getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } memberOfExpression.getLeftExpression().accept(this, context); if (memberOfExpression.isNot()) { builder.append(" NOT MEMBER OF "); @@ -569,21 +638,21 @@ public StringBuilder visit(LongValue longValue, S context) { @Override public StringBuilder visit(MinorThan minorThan, S context) { - deparse(minorThan, " < ", null); + deparse(minorThan, " < ", context); return builder; } @Override public StringBuilder visit(MinorThanEquals minorThanEquals, S context) { - deparse(minorThanEquals, " <= ", null); + deparse(minorThanEquals, " <= ", context); return builder; } @Override public StringBuilder visit(Multiplication multiplication, S context) { - deparse(multiplication, " * ", null); + deparse(multiplication, " * ", context); return builder; } @@ -591,23 +660,21 @@ public StringBuilder visit(Multiplication multiplication, S context) { @Override public StringBuilder visit(NotEqualsTo notEqualsTo, S context) { deparse(notEqualsTo, - " " + notEqualsTo.getStringExpression() + " ", null); + " " + notEqualsTo.getStringExpression() + " ", context); return builder; } @Override public StringBuilder visit(DoubleAnd doubleAnd, S context) { - deparse(doubleAnd, " " + doubleAnd.getStringExpression() + " ", - null); + deparse(doubleAnd, " " + doubleAnd.getStringExpression() + " ", context); return builder; } @Override public StringBuilder visit(Contains contains, S context) { - deparse(contains, " " + contains.getStringExpression() + " ", - null); + deparse(contains, " " + contains.getStringExpression() + " ", context); return builder; } @@ -615,7 +682,7 @@ public StringBuilder visit(Contains contains, S context) { @Override public StringBuilder visit(ContainedBy containedBy, S context) { deparse(containedBy, - " " + containedBy.getStringExpression() + " ", null); + " " + containedBy.getStringExpression() + " ", context); return builder; } @@ -629,14 +696,14 @@ public StringBuilder visit(NullValue nullValue, S context) { @Override public StringBuilder visit(OrExpression orExpression, S context) { - deparse(orExpression, " OR ", null); + deparse(orExpression, " OR ", context); return builder; } @Override public StringBuilder visit(XorExpression xorExpression, S context) { - deparse(xorExpression, " XOR ", null); + deparse(xorExpression, " XOR ", context); return builder; } @@ -648,6 +715,10 @@ public StringBuilder visit(StringValue stringValue, S context) { } builder.append(stringValue.getQuoteStr()).append(stringValue.getValue()) .append(stringValue.getQuoteStr()); + if (stringValue.getUnicodeEscapeCharacter() != null) { + builder.append(" UESCAPE '").append(stringValue.getUnicodeEscapeCharacter()) + .append('\''); + } return builder; } @@ -661,7 +732,7 @@ public StringBuilder visit(BooleanValue booleanValue, S context) { @Override public StringBuilder visit(Subtraction subtraction, S context) { - deparse(subtraction, " - ", null); + deparse(subtraction, " - ", context); return builder; } @@ -676,11 +747,13 @@ protected void deparse(BinaryExpression binaryExpression, @Override public StringBuilder visit(Select select, S context) { if (selectVisitor != null) { - if (select.getWithItemsList() != null) { + // ValuesStatementDeParser owns WITH rendering when using SelectDeParser. + if (select.getWithItemsList() != null + && !(select instanceof Values && selectVisitor instanceof SelectDeParser)) { builder.append("WITH "); for (Iterator> iter = select.getWithItemsList().iterator(); iter .hasNext();) { - iter.next().accept(selectVisitor, null); + iter.next().accept(selectVisitor, context); if (iter.hasNext()) { builder.append(", "); } @@ -689,36 +762,15 @@ public StringBuilder visit(Select select, S context) { builder.append(" "); } - select.accept(selectVisitor, null); + select.accept(selectVisitor, context); } return builder; } @Override public StringBuilder visit(TranscodingFunction transcodingFunction, S context) { - if (transcodingFunction.isTranscodeStyle()) { - builder.append(transcodingFunction.getKeyword()); - builder.append("( "); - transcodingFunction.getExpression().accept(this, context); - builder.append(" USING ") - .append(transcodingFunction.getTranscodingName()) - .append(" )"); - } else { - builder - .append(transcodingFunction.getKeyword()) - .append("( ") - .append(transcodingFunction.getColDataType()) - .append(", "); - transcodingFunction.getExpression().accept(this, context); - - String transCodingName = transcodingFunction.getTranscodingName(); - if (transCodingName != null && !transCodingName.isEmpty()) { - builder.append(", ").append(transCodingName); - } - builder.append(" )"); - } - - return builder; + return transcodingFunction.appendTo(builder, + expression -> expression.accept(this, context)); } public StringBuilder visit(TrimFunction trimFunction, S context) { @@ -812,9 +864,34 @@ public void visit(TrimFunction trimFunction) { @Override public StringBuilder visit(RangeExpression rangeExpression, S context) { - rangeExpression.getStartExpression().accept(this, context); + appendSlice(rangeExpression.getStartExpression(), rangeExpression.getEndExpression(), + rangeExpression.getStepExpression(), context); + return builder; + } + + private void appendSlice(Expression start, Expression end, Expression step, S context) { + if (start != null) { + start.accept(this, context); + } builder.append(":"); - rangeExpression.getEndExpression().accept(this, context); + if (end != null) { + end.accept(this, context); + } else if (step != null) { + builder.append("-"); + } + if (step != null) { + builder.append(":"); + step.accept(this, context); + } + } + + @Override + public StringBuilder visit(TernaryExpression ternaryExpression, S context) { + ternaryExpression.getCondition().accept(this, context); + builder.append(" ? "); + ternaryExpression.getThenExpression().accept(this, context); + builder.append(" : "); + ternaryExpression.getElseExpression().accept(this, context); return builder; } @@ -829,7 +906,9 @@ public StringBuilder visit(Column tableColumn, S context) { tableName = table.getFullyQualifiedName(); } } - if (tableName != null && !tableName.isEmpty()) { + if (tableColumn.getReturningQualifier() != null) { + builder.append(tableColumn.getReturningQualifier()).append("."); + } else if (tableName != null && !tableName.isEmpty()) { builder.append(tableName).append(tableColumn.getTableDelimiter()); } @@ -849,6 +928,15 @@ public StringBuilder visit(Column tableColumn, S context) { return builder; } + private void deParseOrderByElement(OrderByDeParser deParser, + OrderByElement element, S context) { + if (context == null) { + deParser.deParseElement(element); + } else { + deParser.deParseElement(element, context); + } + } + @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder visit(Function function, S context) { @@ -907,7 +995,7 @@ public StringBuilder visit(Function function, S context) { } else { comma = true; } - orderByDeParser.deParseElement(orderByElement); + deParseOrderByElement(orderByDeParser, orderByElement, context); } } @@ -916,7 +1004,7 @@ public StringBuilder visit(Function function, S context) { } if (function.getLimit() != null) { - new LimitDeparser(this, builder).deParse(function.getLimit()); + new LimitDeparser(this, builder).deParse(function.getLimit(), context); } // Generic keyword arguments (e.g. SEPARATOR ',', USING utf8) @@ -957,7 +1045,7 @@ public StringBuilder visit(Function function, S context) { if (function.isEscaped()) { builder.append("}"); } - return builder; + return function.appendResultColumnDefinitionsTo(builder); } @Override @@ -1037,7 +1125,7 @@ public StringBuilder visit(AnyComparisonExpression anyComparisonExpression, @Override public StringBuilder visit(Concat concat, S context) { - deparse(concat, " || ", null); + deparse(concat, " || ", context); return builder; } @@ -1045,6 +1133,10 @@ public void visit(RangeExpression rangeExpression) { visit(rangeExpression, null); } + public void visit(TernaryExpression ternaryExpression) { + visit(ternaryExpression, null); + } + public void visit(Column tableColumn) { visit(tableColumn, null); } @@ -1088,25 +1180,25 @@ public void visit(Concat concat) { @Override public StringBuilder visit(Matches matches, S context) { - deparse(matches, " @@ ", null); + deparse(matches, " @@ ", context); return builder; } @Override public StringBuilder visit(BitwiseAnd bitwiseAnd, S context) { - deparse(bitwiseAnd, " & ", null); + deparse(bitwiseAnd, " & ", context); return builder; } @Override public StringBuilder visit(BitwiseOr bitwiseOr, S context) { - deparse(bitwiseOr, " | ", null); + deparse(bitwiseOr, " | ", context); return builder; } @Override public StringBuilder visit(BitwiseXor bitwiseXor, S context) { - deparse(bitwiseXor, " ^ ", null); + deparse(bitwiseXor, " ^ ", context); return builder; } @@ -1124,7 +1216,7 @@ public StringBuilder visit(CastExpression cast, S context) { cast.getLeftExpression().accept(this, context); builder.append(" AS "); builder.append( - cast.getColumnDefinitions().size() > 1 + !cast.getColumnDefinitions().isEmpty() ? "ROW(" + Select.getStringList(cast.getColumnDefinitions()) + ")" : cast.getColDataType().toString()); builder.append(formatStr); @@ -1139,7 +1231,7 @@ public StringBuilder visit(CastExpression cast, S context) { @Override public StringBuilder visit(Modulo modulo, S context) { - deparse(modulo, " % ", null); + deparse(modulo, " % ", context); return builder; } @@ -1206,7 +1298,7 @@ public StringBuilder visit(AnalyticExpression analyticExpression, S context) } if (analyticExpression.getLimit() != null) { - new LimitDeparser(this, builder).deParse(analyticExpression.getLimit()); + new LimitDeparser(this, builder).deParse(analyticExpression.getLimit(), context); } builder.append(") "); @@ -1215,10 +1307,9 @@ public StringBuilder visit(AnalyticExpression analyticExpression, S context) builder.append(" "); } - if (analyticExpression.getFilterExpression() != null) { - builder.append("FILTER (WHERE "); - analyticExpression.getFilterExpression().accept(this, context); - builder.append(")"); + if (analyticExpression.getFilterExpression() != null + && analyticExpression.getType() != AnalyticType.WITHIN_GROUP) { + analyticExpression.appendFilterTo(builder, filter -> filter.accept(this, context)); if (analyticExpression.getType() != AnalyticType.FILTER_ONLY) { builder.append(" "); } @@ -1259,6 +1350,9 @@ public StringBuilder visit(AnalyticExpression analyticExpression, S context) builder.append(" ").append(analyticExpression.getWindowName()); } else if (analyticExpression.getType() != AnalyticType.WITHIN_GROUP_OVER) { builder.append(" ("); + if (analyticExpression.getWindowDefinition() != null) { + analyticExpression.getWindowDefinition().appendBaseWindowNameTo(builder); + } if (partitionExpressionList != null && !partitionExpressionList.isEmpty()) { @@ -1285,7 +1379,7 @@ public StringBuilder visit(AnalyticExpression analyticExpression, S context) if (i > 0) { builder.append(", "); } - orderByDeParser.deParseElement(orderByElements.get(i)); + deParseOrderByElement(orderByDeParser, orderByElements.get(i), context); } } @@ -1298,6 +1392,11 @@ public StringBuilder visit(AnalyticExpression analyticExpression, S context) builder.append(")"); } + if (analyticExpression.getFilterExpression() != null + && analyticExpression.getType() == AnalyticType.WITHIN_GROUP) { + builder.append(' '); + analyticExpression.appendFilterTo(builder, filter -> filter.accept(this, context)); + } return builder; } @@ -1379,7 +1478,8 @@ public StringBuilder visit(OracleHierarchicalExpression hierarchicalExpressi @Override public StringBuilder visit(RegExpMatchOperator regExpMatchOperator, S context) { - deparse(regExpMatchOperator, " " + regExpMatchOperator.getStringExpression() + " ", null); + deparse(regExpMatchOperator, " " + regExpMatchOperator.getStringExpression() + " ", + context); return builder; } @@ -1392,7 +1492,7 @@ public StringBuilder visit(JsonExpression jsonExpr, S context) { @Override public StringBuilder visit(JsonOperator jsonExpr, S context) { - deparse(jsonExpr, " " + jsonExpr.getStringExpression() + " ", null); + deparse(jsonExpr, " " + jsonExpr.getStringExpression() + " ", context); return builder; } @@ -1402,6 +1502,12 @@ public StringBuilder visit(UserVariable var, S context) { return builder; } + @Override + public StringBuilder visit( + net.sf.jsqlparser.statement.execute.ExecuteArgument argument, S context) { + return argument.appendTo(builder, expression -> expression.accept(this, context)); + } + @Override public StringBuilder visit(NumericBind bind, S context) { builder.append(bind.toString()); @@ -1424,7 +1530,7 @@ public StringBuilder visit(MySQLGroupConcat groupConcat, S context) { public StringBuilder visit(ExpressionList expressionList, S context) { ExpressionListDeParser expressionListDeParser = new ExpressionListDeParser<>(this, builder); - expressionListDeParser.deParse(expressionList); + expressionListDeParser.deParse(expressionList, context); return builder; } @@ -1435,7 +1541,7 @@ public StringBuilder visit(RowConstructor rowConstructor, S context) { } ExpressionListDeParser expressionListDeParser = new ExpressionListDeParser<>(this, builder); - expressionListDeParser.deParse(rowConstructor); + expressionListDeParser.deParse(rowConstructor, context); return builder; } @@ -1446,6 +1552,11 @@ public StringBuilder visit(RowGetExpression rowGetExpression, S context) { return null; } + @Override + public StringBuilder visit(MethodCallExpression methodCall, S context) { + return methodCall.appendTo(builder, expression -> expression.accept(this, context)); + } + @Override public StringBuilder visit(OracleHint hint, S context) { builder.append(hint.toString()); @@ -1480,7 +1591,12 @@ public StringBuilder visit(CollateExpression col, S context) { @Override public StringBuilder visit(SimilarToExpression expr, S context) { - deparse(expr, (expr.isNot() ? " NOT" : "") + " SIMILAR TO ", null); + if (expr.getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } + expr.getLeftExpression().accept(this, context); + builder.append(expr.isNot() ? " NOT SIMILAR TO " : " SIMILAR TO "); + expr.getRightExpression().accept(this, context); return builder; } @@ -1568,13 +1684,8 @@ public StringBuilder visit(ArrayExpression array, S context) { if (array.getIndexExpression() != null) { array.getIndexExpression().accept(this, context); } else { - if (array.getStartIndexExpression() != null) { - array.getStartIndexExpression().accept(this, context); - } - builder.append(":"); - if (array.getStopIndexExpression() != null) { - array.getStopIndexExpression().accept(this, context); - } + appendSlice(array.getStartIndexExpression(), array.getStopIndexExpression(), + array.getStepExpression(), context); } builder.append("]"); @@ -1605,6 +1716,22 @@ public StringBuilder visit(ArrayConstructor arrayConstructor, S context) { return builder; } + @Override + public StringBuilder visit(MapExpression mapExpression, S context) { + builder.append("MAP {"); + for (int i = 0; i < mapExpression.getEntries().size(); i++) { + if (i > 0) { + builder.append(", "); + } + Map.Entry entry = mapExpression.getEntries().get(i); + entry.getKey().accept(this, context); + builder.append(": "); + entry.getValue().accept(this, context); + } + builder.append("}"); + return builder; + } + @Override void deParse(Expression statement) { statement.accept(this, null); @@ -1620,21 +1747,8 @@ public StringBuilder visit(VariableAssignment var, S context) { @Override public StringBuilder visit(XMLSerializeExpr expr, S context) { - // xmlserialize(xmlagg(xmltext(COMMENT_LINE) ORDER BY COMMENT_SEQUENCE) as varchar(1024)) - builder.append("xmlserialize(xmlagg(xmltext("); - expr.getExpression().accept(this, context); - builder.append(")"); - if (expr.getOrderByElements() != null) { - builder.append(" ORDER BY "); - for (Iterator i = expr.getOrderByElements().iterator(); i.hasNext();) { - builder.append(i.next().toString()); - if (i.hasNext()) { - builder.append(", "); - } - } - } - builder.append(") AS ").append(expr.getDataType()).append(")"); - return builder; + return expr.appendTo(builder, expression -> expression.accept(this, context), + orderBy -> new OrderByDeParser(this, builder).deParse(false, orderBy, context)); } @Override @@ -1656,14 +1770,12 @@ public StringBuilder visit(JsonAggregateFunction expression, S context) { @Override public StringBuilder visit(JsonFunction expression, S context) { - expression.append(builder); - return builder; + return expression.appendTo(builder, child -> child.accept(this, context)); } @Override public StringBuilder visit(JsonTableFunction expression, S context) { - builder.append(expression); - return builder; + return expression.appendTo(builder, child -> child.accept(this, context)); } @Override @@ -1728,6 +1840,10 @@ public StringBuilder visit(AllValue allValue, S context) { @Override public StringBuilder visit(IsDistinctExpression isDistinctExpression, S context) { + if (isDistinctExpression + .getOraclePriorPosition() == SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START) { + builder.append("PRIOR "); + } builder.append(isDistinctExpression.getLeftExpression()) .append(isDistinctExpression.getStringExpression()) .append(isDistinctExpression.getRightExpression()); @@ -1737,110 +1853,95 @@ public StringBuilder visit(IsDistinctExpression isDistinctExpression, S cont @Override public StringBuilder visit(GeometryDistance geometryDistance, S context) { deparse(geometryDistance, - " " + geometryDistance.getStringExpression() + " ", null); + " " + geometryDistance.getStringExpression() + " ", context); + return builder; + } + + @Override + public StringBuilder visit(Intersects intersects, S context) { + deparse(intersects, " # ", context); return builder; } @Override public StringBuilder visit(TSQLLeftJoin tsqlLeftJoin, S context) { - this.deparse(tsqlLeftJoin, " *= ", null); + this.deparse(tsqlLeftJoin, " *= ", context); return builder; } @Override public StringBuilder visit(TSQLRightJoin tsqlRightJoin, S context) { - this.deparse(tsqlRightJoin, " =* ", null); + this.deparse(tsqlRightJoin, " =* ", context); return builder; } + @Override + public StringBuilder visit(RowPatternFunction function, S context) { + return function.appendTo(builder, expression -> expression.accept(this, context)); + } + @Override public StringBuilder visit(StructType structType, S context) { - if (structType.getDialect() != StructType.Dialect.DUCKDB - && structType.getKeyword() != null) { - builder.append(structType.getKeyword()); - } - - if (structType.getDialect() != StructType.Dialect.DUCKDB - && structType.getParameters() != null && !structType.getParameters().isEmpty()) { - builder.append("<"); - int i = 0; - for (Map.Entry e : structType.getParameters()) { - if (0 < i++) { - builder.append(","); - } - // optional name - if (e.getKey() != null && !e.getKey().isEmpty()) { - builder.append(e.getKey()).append(" "); - } + return structType.appendTo(builder, expression -> expression.accept(this, context)); + } - // mandatory type - builder.append(e.getValue()); - } + @Override + public StringBuilder visit(LambdaExpression lambdaExpression, S context) { + return lambdaExpression.appendTo(builder, expression -> expression.accept(this, context)); + } - builder.append(">"); - } + @Override + public StringBuilder visit(ListComprehension listComprehension, S context) { + return listComprehension.appendTo(builder, + expression -> expression.accept(this, context)); + } - if (structType.getArguments() != null && !structType.getArguments().isEmpty()) { - if (structType.getDialect() == StructType.Dialect.DUCKDB) { - builder.append("{ "); - int i = 0; - for (SelectItem e : structType.getArguments()) { - if (0 < i++) { - builder.append(","); + @Override + public StringBuilder visit(ColumnsExpression columnsExpression, S context) { + columnsExpression.getColumns().accept(this, context); + for (ColumnsTransformer transformer : columnsExpression.getTransformers()) { + switch (transformer.getType()) { + case APPLY: + builder.append(" APPLY("); + transformer.getApplyExpression().accept(this, context); + builder.append(")"); + break; + case EXCEPT: + builder.append(" EXCEPT"); + if (transformer.isStrict()) { + builder.append(" STRICT"); } - builder.append(e.getAlias().getName()); - builder.append(" : "); - e.getExpression().accept(this, context); - } - builder.append(" }"); - } else { - builder.append("("); - int i = 0; - for (SelectItem e : structType.getArguments()) { - if (0 < i++) { - builder.append(","); + builder.append(" "); + if (transformer.getExceptPattern() != null) { + transformer.getExceptPattern().accept(this, context); + } else { + transformer.getExceptColumns().accept(this, context); } - e.getExpression().accept(this, context); - if (e.getAlias() != null) { - builder.append(" as "); - builder.append(e.getAlias().getName()); + break; + case REPLACE: + builder.append(" REPLACE"); + if (transformer.isStrict()) { + builder.append(" STRICT"); } - } - builder.append(")"); - } - } - - if (structType.getDialect() == StructType.Dialect.DUCKDB - && structType.getParameters() != null && !structType.getParameters().isEmpty()) { - builder.append("::STRUCT( "); - int i = 0; - for (Map.Entry e : structType.getParameters()) { - if (0 < i++) { - builder.append(","); - } - builder.append(e.getKey()).append(" "); - builder.append(e.getValue()); - } - builder.append(")"); - } - return builder; - } - - @Override - public StringBuilder visit(LambdaExpression lambdaExpression, S context) { - if (lambdaExpression.getIdentifiers().size() == 1) { - builder.append(lambdaExpression.getIdentifiers().get(0)); - } else { - int i = 0; - builder.append("( "); - for (String s : lambdaExpression.getIdentifiers()) { - builder.append(i++ > 0 ? ", " : "").append(s); + builder.append("("); + boolean first = true; + for (SelectItem item : transformer.getReplaceItems()) { + if (!first) { + builder.append(", "); + } + first = false; + item.getExpression().accept(this, context); + if (item.getAlias() != null) { + builder.append(item.getAlias()); + } + } + builder.append(")"); + break; + default: + throw new IllegalStateException( + "Unhandled ColumnsTransformerType: " + transformer.getType()); } - builder.append(" )"); } - - builder.append(" -> "); - lambdaExpression.getExpression().accept(this, context); return builder; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java index 62b4c829bf..ee32957246 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java @@ -31,6 +31,10 @@ public ExpressionListDeParser(ExpressionVisitor expressionVisitor @Override public void deParse(ExpressionList expressionList) { + deParse(expressionList, null); + } + + public void deParse(ExpressionList expressionList, S context) { // @todo: remove this NameExpressionList related part String comma = expressionList instanceof NamedExpressionList ? " " @@ -55,7 +59,7 @@ public void deParse(ExpressionList expressionList) { builder.append(name); builder.append(" "); } - expression.accept(expressionVisitor, null); + expression.accept(expressionVisitor, context); i++; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/GrantDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/GrantDeParser.java index 4a0f997f66..97eda71431 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/GrantDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/GrantDeParser.java @@ -2,47 +2,22 @@ * #%L * JSQLParser library * %% - * Copyright (C) 2004 - 2020 JSQLParser + * Copyright (C) 2004 - 2026 JSQLParser * %% * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 * #L% */ package net.sf.jsqlparser.util.deparser; -import java.util.Iterator; - import net.sf.jsqlparser.statement.grant.Grant; public class GrantDeParser extends AbstractDeParser { - public GrantDeParser(StringBuilder buffer) { super(buffer); } @Override public void deParse(Grant grant) { - builder.append("GRANT "); - if (grant.getRole() != null) { - builder.append(grant.getRole()); - } else { - for (Iterator iter = grant.getPrivileges().iterator(); iter.hasNext();) { - String privilege = iter.next(); - builder.append(privilege); - if (iter.hasNext()) { - builder.append(", "); - } - } - builder.append(" ON "); - builder.append(grant.getObjectName()); - } - builder.append(" TO "); - for (Iterator iter = grant.getUsers().iterator(); iter.hasNext();) { - String user = iter.next(); - builder.append(user); - if (iter.hasNext()) { - builder.append(", "); - } - } + grant.appendTo(builder, builder::append); } - } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/GroupByDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/GroupByDeParser.java index fc20bd4cd9..86bcc0ca6e 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/GroupByDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/GroupByDeParser.java @@ -10,41 +10,24 @@ package net.sf.jsqlparser.util.deparser; import net.sf.jsqlparser.expression.ExpressionVisitor; -import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.statement.select.GroupByElement; public class GroupByDeParser extends AbstractDeParser { private final ExpressionListDeParser expressionListDeParser; + private final ExpressionVisitor expressionVisitor; public GroupByDeParser(ExpressionVisitor expressionVisitor, StringBuilder buffer) { super(buffer); + this.expressionVisitor = expressionVisitor; this.expressionListDeParser = new ExpressionListDeParser<>(expressionVisitor, buffer); this.builder = buffer; } @Override - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public void deParse(GroupByElement groupBy) { - builder.append("GROUP BY "); - expressionListDeParser.deParse(groupBy.getGroupByExpressionList()); - - int i = 0; - if (!groupBy.getGroupingSets().isEmpty()) { - if (builder.charAt(builder.length() - 1) != ' ') { - builder.append(' '); - } - builder.append("GROUPING SETS ("); - for (ExpressionList expressionList : groupBy.getGroupingSets()) { - builder.append(i++ > 0 ? ", " : ""); - expressionListDeParser.deParse(expressionList); - } - builder.append(")"); - } - - if (groupBy.isMysqlWithRollup()) { - builder.append(" WITH ROLLUP"); - } + groupBy.appendTo(builder, expressionListDeParser::deParse, + expression -> expression.accept(expressionVisitor, null)); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/InsertDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/InsertDeParser.java index 901e328651..7abcbc0c57 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/InsertDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/InsertDeParser.java @@ -104,8 +104,12 @@ public void deParse(Insert insert) { builder.append(")"); } + if (insert.getColumnMatching() != null) { + builder.append(" ").append(insert.getColumnMatching()); + } + if (insert.isOverriding()) { - builder.append("OVERRIDING SYSTEM VALUE "); + builder.append(" OVERRIDING ").append(insert.getOverridingMode()).append(" VALUE"); } if (insert.getPartitions() != null) { @@ -147,14 +151,13 @@ public void deParse(Insert insert) { builder.append(" ON CONFLICT"); if (insert.getConflictTarget() != null) { - insert.getConflictTarget().appendTo(builder); + insert.getConflictTarget().appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); } insert.getConflictAction().appendTo(builder); } - if (insert.getReturningClause() != null) { - insert.getReturningClause().appendTo(builder); - } + deparseReturningClause(insert.getReturningClause(), expressionVisitor); } public ExpressionVisitor getExpressionVisitor() { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/LimitDeparser.java b/src/main/java/net/sf/jsqlparser/util/deparser/LimitDeparser.java index ff6b6dbf0e..f1d47c4381 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/LimitDeparser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/LimitDeparser.java @@ -22,27 +22,35 @@ public LimitDeparser(ExpressionVisitor expressionVisitor, StringB @Override public void deParse(Limit limit) { + deParse(limit, null); + } + + public void deParse(Limit limit, S context) { builder.append(" LIMIT "); - if (limit.isLimitNull()) { - builder.append("NULL"); + if (limit.isLimitNull() || limit.isLimitAll()) { + builder.append(limit.isLimitNull() ? "NULL" : "ALL"); + appendPercentUnit(limit); } else { - if (limit.isLimitAll()) { - builder.append("ALL"); - } else { - if (null != limit.getOffset()) { - limit.getOffset().accept(expressionVisitor, null); - builder.append(", "); - } - - if (null != limit.getRowCount()) { - limit.getRowCount().accept(expressionVisitor, null); - } + if (null != limit.getOffset()) { + limit.getOffset().accept(expressionVisitor, context); + builder.append(", "); + } + + if (null != limit.getRowCount()) { + limit.getRowCount().accept(expressionVisitor, context); + appendPercentUnit(limit); } } if (limit.getByExpressions() != null) { builder.append(" BY "); - limit.getByExpressions().accept(expressionVisitor, null); + limit.getByExpressions().accept(expressionVisitor, context); + } + } + + private void appendPercentUnit(Limit limit) { + if (limit.getPercentUnit() != null) { + builder.append("%".equals(limit.getPercentUnit()) ? "%" : " " + limit.getPercentUnit()); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java index 003527770f..fba2c17754 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java @@ -60,9 +60,21 @@ public void deParse(Merge merge) { operations.forEach(operation -> operation.accept(this, null)); } + if (merge.getReturningClause() != null) { + merge.getReturningClause().appendTo(builder, + item -> item.accept(selectDeParser, null)); + } + if (merge.getOutputClause() != null) { merge.getOutputClause().appendTo(builder); } + + if (merge.getOption() != null) { + if (merge.getOutputClause() != null) { + builder.setLength(builder.length() - 1); + } + builder.append(merge.getOption()); + } } @Override diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java index 605c54dc28..a45a49ed70 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.select.OrderByElement; +import net.sf.jsqlparser.statement.select.WithFill; public class OrderByDeParser extends AbstractDeParser> { @@ -35,6 +36,11 @@ public void deParse(List orderByElementList) { } public void deParse(boolean oracleSiblings, List orderByElementList) { + deParse(oracleSiblings, orderByElementList, null); + } + + public void deParse(boolean oracleSiblings, List orderByElementList, + S context) { if (oracleSiblings) { builder.append(" ORDER SIBLINGS BY "); } else { @@ -44,7 +50,12 @@ public void deParse(boolean oracleSiblings, List orderByElementL for (Iterator iterator = orderByElementList.iterator(); iterator .hasNext();) { OrderByElement orderByElement = iterator.next(); - deParseElement(orderByElement); + if (context == null) { + // Preserve the customization point used by existing subclasses. + deParseElement(orderByElement); + } else { + deParseElement(orderByElement, context); + } if (iterator.hasNext()) { builder.append(", "); } @@ -52,7 +63,11 @@ public void deParse(boolean oracleSiblings, List orderByElementL } public void deParseElement(OrderByElement orderBy) { - orderBy.getExpression().accept(expressionVisitor, null); + deParseElement(orderBy, null); + } + + public void deParseElement(OrderByElement orderBy, S context) { + orderBy.getExpression().accept(expressionVisitor, context); if (!orderBy.isAsc()) { builder.append(" DESC"); } else if (orderBy.isAscDescPresent()) { @@ -64,11 +79,35 @@ public void deParseElement(OrderByElement orderBy) { ? "NULLS FIRST" : "NULLS LAST"); } + if (orderBy.getWithFill() != null) { + builder.append(' '); + deParseWithFill(orderBy.getWithFill(), context); + } if (orderBy.isMysqlWithRollup()) { builder.append(" WITH ROLLUP"); } } + private void deParseWithFill(WithFill withFill, S context) { + builder.append("WITH FILL"); + if (withFill.getFrom() != null) { + builder.append(" FROM "); + withFill.getFrom().accept(expressionVisitor, context); + } + if (withFill.getTo() != null) { + builder.append(" TO "); + withFill.getTo().accept(expressionVisitor, context); + } + if (withFill.getStep() != null) { + builder.append(" STEP "); + withFill.getStep().accept(expressionVisitor, context); + } + if (withFill.getStaleness() != null) { + builder.append(" STALENESS "); + withFill.getStaleness().accept(expressionVisitor, context); + } + } + void setExpressionVisitor(ExpressionVisitor expressionVisitor) { this.expressionVisitor = expressionVisitor; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/RefreshMaterializedViewStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/RefreshMaterializedViewStatementDeParser.java index 444279ee6a..6c20b72cdb 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/RefreshMaterializedViewStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/RefreshMaterializedViewStatementDeParser.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util.deparser; +import java.util.function.Consumer; +import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; /** @@ -18,36 +20,22 @@ public class RefreshMaterializedViewStatementDeParser extends AbstractDeParser { + private final Consumer
tablePrinter; + public RefreshMaterializedViewStatementDeParser(StringBuilder buffer) { super(buffer); + this.tablePrinter = table -> builder.append(table); + } + + public RefreshMaterializedViewStatementDeParser(StringBuilder buffer, + Consumer
tablePrinter) { + super(buffer); + this.tablePrinter = tablePrinter; } - @SuppressWarnings("PMD.SwitchStmtsShouldHaveDefault") @Override public void deParse(RefreshMaterializedViewStatement view) { - builder.append("REFRESH MATERIALIZED VIEW "); - if (view.getRefreshMode() == null) { - if (view.isConcurrently()) { - builder.append("CONCURRENTLY "); - } - builder.append(view.getView()); - return; - } - switch (view.getRefreshMode()) { - case WITH_DATA: - if (view.isConcurrently()) { - builder.append("CONCURRENTLY "); - } - builder.append(view.getView()); - builder.append(" WITH DATA"); - break; - case WITH_NO_DATA: - builder.append(view.getView()); - if (view.isConcurrently()) { - builder.append(" WITH NO DATA"); - } - break; - } + view.appendTo(builder, tablePrinter); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 50edf3b70a..3fee43edf6 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.select.MatchRecognize; + import static java.util.stream.Collectors.joining; import java.lang.reflect.InvocationTargetException; @@ -51,6 +53,7 @@ import net.sf.jsqlparser.statement.select.First; import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.FromItemVisitor; +import net.sf.jsqlparser.statement.select.InterpolateElement; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; import net.sf.jsqlparser.statement.select.LateralView; @@ -61,6 +64,8 @@ import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Pivot; +import net.sf.jsqlparser.statement.select.PivotQuery; +import net.sf.jsqlparser.statement.select.UnPivotQuery; import net.sf.jsqlparser.statement.select.PivotVisitor; import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -77,7 +82,6 @@ import net.sf.jsqlparser.statement.select.UnPivot; import net.sf.jsqlparser.statement.select.Values; import net.sf.jsqlparser.statement.select.WithItem; -import net.sf.jsqlparser.statement.update.UpdateSet; @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public class SelectDeParser extends AbstractDeParser @@ -136,6 +140,11 @@ public StringBuilder visit(ParenthesedSelect select, S context) { if (select.getOrderByElements() != null) { new OrderByDeParser(expressionVisitor, builder).deParse(select.isOracleSiblings(), select.getOrderByElements()); + deParseInterpolate(select.getInterpolate()); + } + + if (select.getOption() != null) { + builder.append(select.getOption()); } Alias alias = select.getAlias(); @@ -192,105 +201,16 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { } } - builder.append("SELECT "); - - if (plainSelect.getMySqlHintStraightJoin()) { - builder.append("STRAIGHT_JOIN "); - } - - OracleHint hint = plainSelect.getOracleHint(); - if (hint != null) { - builder.append(hint).append(" "); - } - - Skip skip = plainSelect.getSkip(); - if (skip != null) { - builder.append(skip).append(" "); - } - - First first = plainSelect.getFirst(); - if (first != null) { - builder.append(first).append(" "); - } - - deparseDistinctClause(plainSelect.getDistinct()); - - if (plainSelect.getBigQuerySelectQualifier() != null) { - switch (plainSelect.getBigQuerySelectQualifier()) { - case AS_STRUCT: - builder.append("AS STRUCT "); - break; - case AS_VALUE: - builder.append("AS VALUE "); - break; - } - } - - Top top = plainSelect.getTop(); - if (top != null) { - visit(top); - } - - if (plainSelect.getMySqlSqlCacheFlag() != null) { - builder.append(plainSelect.getMySqlSqlCacheFlag().name()).append(" "); - } - - if (plainSelect.getMySqlSqlCalcFoundRows()) { - builder.append("SQL_CALC_FOUND_ROWS").append(" "); - } - - deparseSelectItemsClause(plainSelect.getSelectItems()); - - if (plainSelect.getIntoTables() != null) { - builder.append(" INTO "); - for (Iterator
iter = plainSelect.getIntoTables().iterator(); iter.hasNext();) { - visit(iter.next(), context); - if (iter.hasNext()) { - builder.append(", "); - } - } - } - - if (plainSelect.getMySqlSelectIntoClause() != null - && plainSelect.getMySqlSelectIntoClause() - .getPosition() == MySqlSelectIntoClause.Position.BEFORE_FROM) { - builder.append(" ").append(plainSelect.getMySqlSelectIntoClause()); - } - - if (plainSelect.getFromItem() != null) { - builder.append(" FROM "); - if (plainSelect.isUsingOnly()) { - builder.append("ONLY "); - } - plainSelect.getFromItem().accept(this, context); - - if (plainSelect.getFromItem() instanceof Table) { - Table table = (Table) plainSelect.getFromItem(); - if (table.getSampleClause() != null) { - table.getSampleClause().appendTo(builder); - } - } - } - - if (plainSelect.getLateralViews() != null) { - for (LateralView lateralView : plainSelect.getLateralViews()) { - deparseLateralView(lateralView); - } - } - - if (plainSelect.getJoins() != null) { - for (Join join : plainSelect.getJoins()) { - deparseJoin(join); + if (plainSelect.isFromFirst()) { + // DuckDB allows the FROM clause to lead, with an optional SELECT clause behind it + deparseFromClause(plainSelect, context); + if (plainSelect.getSelectItems() != null && !plainSelect.getSelectItems().isEmpty()) { + builder.append(" "); + deparseSelectClause(plainSelect, context); } - } - - if (plainSelect.isUsingFinal()) { - builder.append(" FINAL"); - } - - if (plainSelect.getKsqlWindow() != null) { - builder.append(" WINDOW "); - builder.append(plainSelect.getKsqlWindow().toString()); + } else { + deparseSelectClause(plainSelect, context); + deparseFromClause(plainSelect, context); } deparsePreWhereClause(plainSelect); @@ -313,7 +233,7 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { builder.append(" HAVING "); plainSelect.getHaving().accept(expressionVisitor, context); } - if (plainSelect.getQualify() != null) { + if (plainSelect.getQualify() != null && !plainSelect.isQualifyAfterWindow()) { builder.append(" QUALIFY "); plainSelect.getQualify().accept(expressionVisitor, context); } @@ -322,6 +242,10 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { builder.append(plainSelect.getWindowDefinitions().stream() .map(WindowDefinition::toString).collect(joining(", "))); } + if (plainSelect.getQualify() != null && plainSelect.isQualifyAfterWindow()) { + builder.append(" QUALIFY "); + plainSelect.getQualify().accept(expressionVisitor, context); + } Alias alias = plainSelect.getAlias(); if (alias != null) { builder.append(alias); @@ -343,9 +267,11 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { plainSelect.getForClause().appendTo(builder); } - if (plainSelect.isEmitChanges()) { - builder.append(" EMIT CHANGES"); + if (plainSelect.getOption() != null) { + builder.append(plainSelect.getOption()); } + + plainSelect.appendEmitClauseTo(builder); if (plainSelect.getLimitBy() != null) { new LimitDeparser(expressionVisitor, builder).deParse(plainSelect.getLimitBy()); } @@ -355,6 +281,9 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { if (plainSelect.getOffset() != null) { visit(plainSelect.getOffset()); } + if (plainSelect.getMySqlProcedureAnalyse() != null) { + plainSelect.getMySqlProcedureAnalyse().appendTo(builder); + } if (plainSelect.getFetch() != null) { visit(plainSelect.getFetch()); } @@ -389,11 +318,7 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { if (plainSelect.isForUpdateBeforeOrderBy()) { deparseOrderByElementsClause(plainSelect, plainSelect.getOrderByElements()); } - if (plainSelect.getMySqlSelectIntoClause() != null - && plainSelect.getMySqlSelectIntoClause() - .getPosition() == MySqlSelectIntoClause.Position.TRAILING) { - builder.append(" ").append(plainSelect.getMySqlSelectIntoClause()); - } + deparseMySqlSelectInto(plainSelect, MySqlSelectIntoClause.Position.TRAILING, context); if (plainSelect.getSettings() != null && !plainSelect.getSettings().isEmpty()) { builder.append(" SETTINGS "); deparseUpdateSets(plainSelect.getSettings(), builder, expressionVisitor); @@ -416,6 +341,190 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { return builder; } + private void deparseSelectClause(PlainSelect plainSelect, S context) { + builder.append("SELECT "); + + OracleHint hint = plainSelect.getOracleHint(); + if (hint != null) { + builder.append(hint).append(" "); + } + + Skip skip = plainSelect.getSkip(); + if (skip != null) { + builder.append(skip).append(" "); + } + + First first = plainSelect.getFirst(); + if (first != null) { + builder.append(first).append(" "); + } + + deparseDistinctClause(plainSelect.getDistinct()); + + if (plainSelect.getBigQuerySelectQualifier() != null) { + switch (plainSelect.getBigQuerySelectQualifier()) { + case AS_STRUCT: + builder.append("AS STRUCT "); + break; + case AS_VALUE: + builder.append("AS VALUE "); + break; + } + } + + Top top = plainSelect.getTop(); + if (top != null) { + visit(top); + } + + plainSelect.appendMySqlSelectModifiersTo(builder); + + int beforeItems = builder.length(); + deparseSelectItemsClause(plainSelect.getSelectItems()); + // PostgreSQL permits SELECT with no target expressions: keep "SELECT + // FROM" instead of doubling the space. + if (builder.length() == beforeItems && builder.charAt(beforeItems - 1) == ' ') { + builder.deleteCharAt(beforeItems - 1); + } + + if (plainSelect.getIntoTables() != null) { + builder.append(" INTO "); + for (Iterator
iter = plainSelect.getIntoTables().iterator(); iter.hasNext();) { + visit(iter.next(), context); + if (iter.hasNext()) { + builder.append(", "); + } + } + } + + deparseMySqlSelectInto(plainSelect, MySqlSelectIntoClause.Position.BEFORE_FROM, context); + } + + private void deparseFromClause(PlainSelect plainSelect, S context) { + if (plainSelect.getFromItem() != null) { + builder.append(plainSelect.isFromFirst() ? "FROM " : " FROM "); + if (plainSelect.isUsingOnly()) { + builder.append("ONLY "); + } + plainSelect.getFromItem().accept(this, context); + + if (plainSelect.getFromItem() instanceof Table) { + Table table = (Table) plainSelect.getFromItem(); + if (table.getSampleClause() != null) { + table.getSampleClause().appendTo(builder); + } + } + } + + if (plainSelect.getLateralViews() != null) { + for (LateralView lateralView : plainSelect.getLateralViews()) { + deparseLateralView(lateralView); + } + } + + if (plainSelect.getJoins() != null) { + for (Join join : plainSelect.getJoins()) { + deparseJoin(join); + } + } + + if (plainSelect.isUsingFinal()) { + builder.append(" FINAL"); + } + + if (plainSelect.getKsqlWindow() != null) { + builder.append(" WINDOW "); + builder.append(plainSelect.getKsqlWindow().toString()); + } + } + + + @Override + public StringBuilder visit(UnPivotQuery unPivotQuery, S context) { + unPivotQuery.appendSelectBodyTo(builder); + return builder; + } + + @Override + public void visit(UnPivotQuery unPivotQuery) { + SelectVisitor.super.visit(unPivotQuery); + } + + @Override + public StringBuilder visit(PivotQuery pivotQuery, S context) { + List> withItemsList = pivotQuery.getWithItemsList(); + if (withItemsList != null && !withItemsList.isEmpty()) { + builder.append("WITH "); + for (Iterator> iter = withItemsList.iterator(); iter.hasNext();) { + iter.next().accept((SelectVisitor) this, context); + if (iter.hasNext()) { + builder.append(","); + } + builder.append(" "); + } + } + + builder.append("PIVOT "); + pivotQuery.getFromItem().accept(this, context); + + if (pivotQuery.getOnExpressions() != null) { + builder.append(" ON "); + pivotQuery.getOnExpressions().accept(expressionVisitor, context); + } + if (pivotQuery.getUsingItems() != null) { + builder.append(" USING "); + for (Iterator> iter = + pivotQuery.getUsingItems().iterator(); iter.hasNext();) { + iter.next().accept(this, context); + if (iter.hasNext()) { + builder.append(", "); + } + } + } + if (pivotQuery.getGroupByExpressions() != null) { + builder.append(" GROUP BY "); + pivotQuery.getGroupByExpressions().accept(expressionVisitor, context); + } + + Alias alias = pivotQuery.getAlias(); + if (alias != null) { + builder.append(alias); + } + Pivot pivot = pivotQuery.getPivot(); + if (pivot != null) { + pivot.accept(this, context); + } + UnPivot unpivot = pivotQuery.getUnPivot(); + if (unpivot != null) { + unpivot.accept(this, context); + } + + if (pivotQuery.getOrderByElements() != null) { + new OrderByDeParser(expressionVisitor, builder).deParse(pivotQuery.isOracleSiblings(), + pivotQuery.getOrderByElements()); + deParseInterpolate(pivotQuery.getInterpolate()); + } + if (pivotQuery.getOption() != null) { + builder.append(pivotQuery.getOption()); + } + if (pivotQuery.getLimitBy() != null) { + new LimitDeparser(expressionVisitor, builder).deParse(pivotQuery.getLimitBy()); + } + if (pivotQuery.getLimit() != null) { + new LimitDeparser(expressionVisitor, builder).deParse(pivotQuery.getLimit()); + } + if (pivotQuery.getOffset() != null) { + visit(pivotQuery.getOffset()); + } + if (pivotQuery.getFetch() != null) { + visit(pivotQuery.getFetch()); + } + if (pivotQuery.getIsolation() != null) { + builder.append(pivotQuery.getIsolation()); + } + return builder; + } + protected void deparseWhereClause(PlainSelect plainSelect) { if (plainSelect.getWhere() != null) { builder.append(" WHERE "); @@ -471,9 +580,33 @@ protected void deparseOrderByElementsClause(PlainSelect plainSelect, if (orderByElements != null) { new OrderByDeParser(expressionVisitor, builder).deParse(plainSelect.isOracleSiblings(), orderByElements); + deParseInterpolate(plainSelect.getInterpolate()); } } + private void deParseInterpolate(List interpolate) { + if (interpolate == null) { + return; + } + if (interpolate.isEmpty()) { + builder.append(" INTERPOLATE"); + return; + } + builder.append(" INTERPOLATE ("); + for (Iterator iter = interpolate.iterator(); iter.hasNext();) { + InterpolateElement element = iter.next(); + element.getColumn().accept(expressionVisitor, null); + if (element.getExpression() != null) { + builder.append(" AS "); + element.getExpression().accept(expressionVisitor, null); + } + if (iter.hasNext()) { + builder.append(", "); + } + } + builder.append(")"); + } + @Override public StringBuilder visit(SelectItem selectItem, S context) { selectItem.getExpression().accept(expressionVisitor, context); @@ -484,6 +617,15 @@ public StringBuilder visit(SelectItem selectItem, S context) { } + @Override + public StringBuilder visit(MatchRecognize matchRecognize, S context) { + OrderByDeParser ordering = new OrderByDeParser(expressionVisitor, builder); + return matchRecognize.appendTo(builder, input -> input.accept(this, context), + expression -> expression.accept(expressionVisitor, context), + order -> ordering.deParseElement(order, context), + pivot -> pivot.accept(this, context), unpivot -> unpivot.accept(this, context)); + } + @Override public StringBuilder visit(Table table, S context) { builder.append(table.getFullyQualifiedName()); @@ -609,6 +751,15 @@ public void setExpressionVisitor(ExpressionVisitor visitor) { expressionVisitor = visitor; } + private void deparseMySqlSelectInto(PlainSelect select, + MySqlSelectIntoClause.Position position, S context) { + MySqlSelectIntoClause into = select.getMySqlSelectIntoClause(); + if (into != null && into.getPosition() == position) { + builder.append(' '); + into.appendTo(builder, expression -> expression.accept(expressionVisitor, context)); + } + } + @SuppressWarnings({"PMD.CyclomaticComplexity"}) public void deparseJoin(Join join) { if (join.isGlobal()) { @@ -621,47 +772,9 @@ public void deparseJoin(Join join) { builder.append(", "); } else { - if (join.isNatural()) { - builder.append(" NATURAL"); - } - - if (join.isAny()) { - builder.append(" ANY"); - } else if (join.isAll()) { - builder.append(" ALL"); - } - - if (join.isRight()) { - builder.append(" RIGHT"); - } else if (join.isFull()) { - builder.append(" FULL"); - } else if (join.isLeft()) { - builder.append(" LEFT"); - } else if (join.isCross()) { - builder.append(" CROSS"); - } - - if (join.isOuter()) { - builder.append(" OUTER"); - } else if (join.isInner()) { - builder.append(" INNER"); - } else if (join.isSemi()) { - builder.append(" SEMI"); - } - - if (join.isStraight()) { - builder.append(" STRAIGHT_JOIN "); - } else if (join.isApply()) { - builder.append(" APPLY "); - } else { - if (join.getJoinHint() != null) { - builder.append(" ").append(join.getJoinHint()); - } - builder.append(" JOIN "); - if (join.isFetch()) { - builder.append("FETCH "); - } - } + builder.append(' '); + join.appendJoinTypeTo(builder); + join.appendJoinKeywordTo(builder); } @@ -671,6 +784,12 @@ public void deparseJoin(Join join) { builder.append(" WITHIN "); builder.append(join.getJoinWindow().toString()); } + if (join.getApproxNearest() != null) { + builder.append(" APPROX NEAREST ").append(join.getApproxNearest()) + .append(" BY SIMILARITY "); + join.getSimilarity().accept(expressionVisitor, null); + } + for (Expression onExpression : join.getOnExpressions()) { builder.append(" ON "); onExpression.accept(expressionVisitor, null); @@ -729,6 +848,11 @@ public StringBuilder visit(SetOperationList list, S context) { } if (list.getOrderByElements() != null) { new OrderByDeParser(expressionVisitor, builder).deParse(list.getOrderByElements()); + deParseInterpolate(list.getInterpolate()); + } + + if (list.getOption() != null) { + builder.append(list.getOption()); } if (list.getLimit() != null) { @@ -762,31 +886,8 @@ public StringBuilder visit(SetOperationList list, S context) { @Override public StringBuilder visit(WithItem withItem, S context) { - if (withItem.getWithFunctionDeclaration() == null) { - if (withItem.isRecursive()) { - builder.append("RECURSIVE "); - } - builder.append(withItem.getAlias().getName()); - if (withItem.getWithItemList() != null) { - builder.append(" ") - .append(PlainSelect.getStringList(withItem.getWithItemList(), true, true)); - } - builder.append(" AS "); - if (withItem.isMaterialized()) { - builder.append(withItem.isUsingNot() - ? "NOT MATERIALIZED " - : "MATERIALIZED "); - } - StatementDeParser statementDeParser = - new StatementDeParser((ExpressionDeParser) expressionVisitor, this, builder); - statementDeParser.deParse(withItem.getParenthesedStatement()); - if (withItem.getSearchClause() != null) { - builder.append(" ").append(withItem.getSearchClause()); - } - } else { - builder.append(withItem.getWithFunctionDeclaration().toString()); - } - return builder; + return new DmlDeParserSupport(expressionVisitor, this, builder) + .deparseWithItem(withItem, context); } @Override @@ -805,7 +906,8 @@ public StringBuilder visit(TableStatement tableStatement, S context) { @Override public StringBuilder visit(TableFunction tableFunction, S context) { - tableFunction.appendTo(builder); + tableFunction.appendTo(builder, + expression -> expression.accept(expressionVisitor, context)); return builder; } @@ -842,7 +944,7 @@ public StringBuilder visit(ParenthesedFromItem fromItem, S context) { @Override public StringBuilder visit(Values values, S context) { - new ValuesStatementDeParser(expressionVisitor, builder).deParse(values); + new ValuesStatementDeParser(expressionVisitor, this, builder).deParse(values, context); return builder; } @@ -865,6 +967,11 @@ public void visit(PlainSelect plainSelect) { visit(plainSelect, null); } + @Override + public void visit(PivotQuery pivotQuery) { + visit(pivotQuery, null); + } + public void visit(SelectItem selectExpressionItem) { visit(selectExpressionItem, null); } @@ -1045,9 +1152,11 @@ public StringBuilder visit(JoinPipeOperator join, Void context) { @Override public StringBuilder visit(LimitPipeOperator limit, Void context) { - builder.append("|> ").append("LIMIT ").append(limit.getLimitExpression()); + builder.append("|> LIMIT "); + limit.getLimitExpression().accept(expressionVisitor, context); if (limit.getOffsetExpression() != null) { - builder.append(" OFFSET ").append(limit.getOffsetExpression()); + builder.append(" OFFSET "); + limit.getOffsetExpression().accept(expressionVisitor, context); } return builder; } @@ -1092,7 +1201,8 @@ public StringBuilder visit(SelectPipeOperator select, Void context) { int i = 0; for (SelectItem selectItem : select.getSelectItems()) { - builder.append(i++ > 0 ? ", " : " ").append(selectItem); + builder.append(i++ > 0 ? ", " : " "); + selectItem.accept(this, context); } builder.append("\n"); return builder; @@ -1100,10 +1210,10 @@ public StringBuilder visit(SelectPipeOperator select, Void context) { @Override public StringBuilder visit(SetPipeOperator set, Void context) { - builder.append("|> ").append("SET"); - int i = 0; - for (UpdateSet updateSet : set.getUpdateSets()) { - builder.append(i++ > 0 ? ", " : " ").append(updateSet); + builder.append("|> SET"); + if (!set.getUpdateSets().isEmpty()) { + builder.append(' '); + deparseUpdateSets(set.getUpdateSets(), builder, expressionVisitor); } builder.append("\n"); return builder; diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SetStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SetStatementDeParser.java index 31ff0f5997..9dee3a23a8 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SetStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SetStatementDeParser.java @@ -9,11 +9,9 @@ */ package net.sf.jsqlparser.util.deparser; -import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.SetStatement; -import java.util.List; public class SetStatementDeParser extends AbstractDeParser { @@ -27,28 +25,7 @@ public SetStatementDeParser(ExpressionVisitor expressionVisitor, @Override public void deParse(SetStatement set) { - builder.append("SET "); - if (set.getEffectParameter() != null) { - builder.append(set.getEffectParameter()).append(" "); - } - for (int i = 0; i < set.getCount(); i++) { - if (i > 0) { - builder.append(", "); - } - builder.append(set.getName(i)); - if (set.isUseEqual(i)) { - builder.append(" ="); - } - builder.append(" "); - List expressions = set.getExpressions(i); - for (int j = 0; j < expressions.size(); j++) { - if (j > 0) { - builder.append(", "); - } - expressions.get(j).accept(expressionVisitor, null); - } - } - + set.appendTo(builder, expression -> expression.accept(expressionVisitor, null)); } public ExpressionVisitor getExpressionVisitor() { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ShowColumnsStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ShowColumnsStatementDeParser.java index 9fc744010a..1bc8534a53 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ShowColumnsStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ShowColumnsStatementDeParser.java @@ -19,6 +19,6 @@ public ShowColumnsStatementDeParser(StringBuilder buffer) { @Override public void deParse(ShowColumnsStatement show) { - builder.append("SHOW COLUMNS FROM ").append(show.getTableName()); + builder.append(show); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 751c4bf64d..6a3e88c7cd 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -9,16 +9,63 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; +import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterRoutine; +import net.sf.jsqlparser.statement.alter.AlterTrigger; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; +import net.sf.jsqlparser.statement.alter.AlterStatistics; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespace; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.ReassignOwned; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; +import net.sf.jsqlparser.statement.create.type.CreateType; +import net.sf.jsqlparser.statement.alter.AlterType; +import net.sf.jsqlparser.statement.create.domain.CreateDomain; +import net.sf.jsqlparser.statement.alter.AlterDomain; +import net.sf.jsqlparser.statement.create.extension.CreateExtension; +import net.sf.jsqlparser.statement.alter.AlterExtension; +import net.sf.jsqlparser.statement.create.publication.CreatePublication; +import net.sf.jsqlparser.statement.alter.AlterPublication; +import net.sf.jsqlparser.statement.create.subscription.CreateSubscription; +import net.sf.jsqlparser.statement.alter.AlterSubscription; + import java.lang.reflect.InvocationTargetException; import java.util.List; -import java.util.stream.Collectors; -import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Block; import net.sf.jsqlparser.statement.Commit; +import net.sf.jsqlparser.statement.StartTransaction; +import net.sf.jsqlparser.statement.ReleaseSavepointStatement; import net.sf.jsqlparser.statement.CreateFunctionalStatement; import net.sf.jsqlparser.statement.DeclareStatement; import net.sf.jsqlparser.statement.DescribeStatement; +import net.sf.jsqlparser.statement.DoStatement; import net.sf.jsqlparser.statement.ExplainStatement; import net.sf.jsqlparser.statement.IfElseStatement; import net.sf.jsqlparser.statement.PurgeStatement; @@ -27,6 +74,9 @@ import net.sf.jsqlparser.statement.SavepointStatement; import net.sf.jsqlparser.statement.SessionStatement; import net.sf.jsqlparser.statement.SetStatement; +import net.sf.jsqlparser.statement.SetDefaultRoleStatement; +import net.sf.jsqlparser.statement.SetPasswordStatement; +import net.sf.jsqlparser.statement.SetRoleStatement; import net.sf.jsqlparser.statement.ShowColumnsStatement; import net.sf.jsqlparser.statement.ShowStatement; import net.sf.jsqlparser.statement.Statement; @@ -34,19 +84,31 @@ import net.sf.jsqlparser.statement.Statements; import net.sf.jsqlparser.statement.UnsupportedStatement; import net.sf.jsqlparser.statement.UseStatement; +import net.sf.jsqlparser.statement.SetIdentityInsertStatement; +import net.sf.jsqlparser.statement.TableMaintenanceStatement; import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.statement.alter.AlterSession; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; +import net.sf.jsqlparser.statement.alter.AlterUser; +import net.sf.jsqlparser.statement.alter.RenameUser; 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.event.AlterEvent; +import net.sf.jsqlparser.statement.create.event.CreateEvent; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -57,19 +119,38 @@ import net.sf.jsqlparser.statement.grant.Grant; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.insert.InsertBulk; import net.sf.jsqlparser.statement.insert.ParenthesedInsert; import net.sf.jsqlparser.statement.lock.LockStatement; import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectVisitor; import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.statement.PragmaStatement; +import net.sf.jsqlparser.statement.ExtensionStatement; +import net.sf.jsqlparser.statement.AttachStatement; +import net.sf.jsqlparser.statement.DetachStatement; +import net.sf.jsqlparser.statement.ConnectStatement; +import net.sf.jsqlparser.statement.DisconnectStatement; +import net.sf.jsqlparser.statement.PrepareStatement; +import net.sf.jsqlparser.statement.DeallocateStatement; +import net.sf.jsqlparser.statement.CopyStatement; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.AssertStatement; +import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; public class StatementDeParser extends AbstractDeParser implements StatementVisitor { @@ -121,11 +202,24 @@ public StatementDeParser(ExpressionDeParser expressionDeParser, SelectDeParser s @Override public StringBuilder visit(CreateIndex createIndex, S context) { - CreateIndexDeParser createIndexDeParser = new CreateIndexDeParser(builder); + CreateIndexDeParser createIndexDeParser = + new CreateIndexDeParser(builder, expressionDeParser); createIndexDeParser.deParse(createIndex); return builder; } + @Override + public StringBuilder visit(DoStatement statement, S context) { + return statement.appendTo(builder, code -> code.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(InsertBulk statement, S context) { + OrderByDeParser order = new OrderByDeParser(expressionDeParser, builder); + return statement.appendTo(builder, value -> value.accept(expressionDeParser, context), + element -> order.deParseElement(element, context)); + } + @Override public StringBuilder visit(CreateTable createTable, S context) { CreateTableDeParser createTableDeParser = new CreateTableDeParser(this, builder); @@ -133,9 +227,39 @@ public StringBuilder visit(CreateTable createTable, S context) { return builder; } + @Override + public StringBuilder visit(CreateTrigger createTrigger, S context) { + createTrigger.appendTo(builder, e -> e.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(CreateEventTrigger statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterEventTrigger statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(Reindex statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(SecurityLabel statement, S context) { + return statement.appendTo(builder, + table -> table.accept(selectDeParser, context), + column -> column.accept(expressionDeParser, context), + label -> label.accept(expressionDeParser, context)); + } + @Override public StringBuilder visit(CreateView createView, S context) { - CreateViewDeParser createViewDeParser = new CreateViewDeParser(builder); + CreateViewDeParser createViewDeParser = new CreateViewDeParser(builder, selectDeParser); createViewDeParser.deParse(createView); return builder; } @@ -143,7 +267,9 @@ public StringBuilder visit(CreateView createView, S context) { @Override public StringBuilder visit(RefreshMaterializedViewStatement materializedViewStatement, S context) { - new RefreshMaterializedViewStatementDeParser(builder).deParse(materializedViewStatement); + selectDeParser.setBuilder(builder); + new RefreshMaterializedViewStatementDeParser(builder, + table -> table.accept(selectDeParser, context)).deParse(materializedViewStatement); return builder; } @@ -156,18 +282,26 @@ public StringBuilder visit(AlterView alterView, S context) { @Override public StringBuilder visit(Delete delete, S context) { - DeleteDeParser deleteDeParser = new DeleteDeParser(expressionDeParser, builder); + DeleteDeParser deleteDeParser = + new DeleteDeParser(expressionDeParser, selectDeParser, builder); deleteDeParser.deParse(delete); return builder; } @Override public StringBuilder visit(Drop drop, S context) { - DropDeParser dropDeParser = new DropDeParser(builder); + DropDeParser dropDeParser = + new DropDeParser(builder, table -> table.accept(selectDeParser, context)); dropDeParser.deParse(drop); return builder; } + @Override + public StringBuilder visit(ReassignOwned statement, S context) { + builder.append(statement); + return builder; + } + @Override public StringBuilder visit(Insert insert, S context) { InsertDeParser insertDeParser = @@ -231,55 +365,47 @@ public StringBuilder visit(Select select, S context) { @Override public StringBuilder visit(Truncate truncate, S context) { - builder.append("TRUNCATE"); - if (truncate.isTableToken()) { - builder.append(" TABLE"); - } - if (truncate.isOnly()) { - builder.append(" ONLY"); - } - builder.append(" "); - if (truncate.getTables() != null && !truncate.getTables().isEmpty()) { - builder.append(truncate.getTables().stream() - .map(Table::toString) - .collect(Collectors.joining(", "))); - } else { - builder.append(truncate.getTable()); - } - if (truncate.getCascade()) { - builder.append(" CASCADE"); - } - - return builder; + return truncate.appendTo(builder); } @Override public StringBuilder visit(Update update, S context) { - UpdateDeParser updateDeParser = new UpdateDeParser(expressionDeParser, builder); + UpdateDeParser updateDeParser = + new UpdateDeParser(expressionDeParser, selectDeParser, builder); updateDeParser.deParse(update); return builder; } public StringBuilder visit(Analyze analyzer, S context) { - builder.append("ANALYZE "); - builder.append(analyzer.getTable()); + builder.append(analyzer); + return builder; + } + + @Override + public StringBuilder visit(TableMaintenanceStatement statement, S context) { + builder.append(statement); return builder; } @Override public StringBuilder visit(Alter alter, S context) { - AlterDeParser alterDeParser = new AlterDeParser(builder); + AlterDeParser alterDeParser = new AlterDeParser(builder, expressionDeParser); alterDeParser.deParse(alter); return builder; } @Override - public StringBuilder visit(Statements statements, S context) { - statements.accept(this, context); + public StringBuilder visit(AlterEvent alterEvent, S context) { + builder.append(alterEvent); return builder; } + @Override + public StringBuilder visit(Statements statements, S context) { + return statements.appendTo(builder, statement -> statement.accept(this, context)); + } + @Override public StringBuilder visit(Execute execute, S context) { ExecuteDeParser executeDeParser = new ExecuteDeParser(expressionDeParser, builder); @@ -322,6 +448,16 @@ public StringBuilder visit(RollbackStatement rollbackStatement, S context) { return builder; } + @Override + public StringBuilder visit(StartTransaction statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(ReleaseSavepointStatement statement, S context) { + return statement.appendTo(builder); + } + @Override public StringBuilder visit(Commit commit, S context) { builder.append(commit.toString()); @@ -336,6 +472,11 @@ public StringBuilder visit(Upsert upsert, S context) { return builder; } + @Override + public StringBuilder visit(SetIdentityInsertStatement statement, S context) { + return statement.appendTo(builder, table -> table.accept(selectDeParser, context)); + } + @Override public StringBuilder visit(UseStatement use, S context) { new UseStatementDeParser(builder).deParse(use); @@ -361,44 +502,49 @@ public StringBuilder visit(ShowTablesStatement showTables, S context) { } @Override - public StringBuilder visit(Block block, S context) { - builder.append("BEGIN\n"); - if (block.getStatements() != null) { - for (Statement stmt : block.getStatements()) { - stmt.accept(this, context); - builder.append(";\n"); - } - } - builder.append("END"); - if (block.hasSemicolonAfterEnd()) { - builder.append(";"); - } + public StringBuilder visit(ShowTableStatusStatement showTableStatus, S context) { + builder.append(showTableStatus); return builder; } @Override - public StringBuilder visit(Comment comment, S context) { - builder.append(comment.toString()); + public StringBuilder visit(ShowCreateStatement showCreate, S context) { + builder.append(showCreate); return builder; } + @Override + public StringBuilder visit(Block block, S context) { + return block.appendTo(builder, statements -> statements.accept(this, context)); + } + + @Override + public StringBuilder visit(Comment comment, S context) { + return comment.appendTo(builder, + table -> table.accept(selectDeParser, context), + column -> column.accept(expressionDeParser, context), + literal -> literal.accept(expressionDeParser, context)); + } + @Override public StringBuilder visit(DescribeStatement describe, S context) { builder.append(describe.getDescribeType()); builder.append(" "); - builder.append(describe.getTable()); + if (describe.getSelect() != null) { + describe.getSelect().accept(this, context); + } else { + builder.append(describe.getTable()); + } return builder; } @Override public StringBuilder visit(ExplainStatement explainStatement, S context) { - builder.append(explainStatement.getKeyword()).append(" "); + builder.append(explainStatement.getKeyword()); if (explainStatement.getTable() != null) { - builder.append(explainStatement.getTable()); - } else if (explainStatement.getOptions() != null) { - builder.append(explainStatement.getOptions().values().stream() - .map(ExplainStatement.Option::formatOption).collect(Collectors.joining(" "))); - builder.append(" "); + builder.append(" ").append(explainStatement.getTable()); + } else { + explainStatement.appendOptionsTo(builder).append(" "); } if (explainStatement.getStatement() != null) { explainStatement.getStatement().accept(this, context); @@ -420,17 +566,62 @@ public StringBuilder visit(DeclareStatement declareStatement, S context) { @Override public StringBuilder visit(Grant grant, S context) { - GrantDeParser grantDeParser = new GrantDeParser(builder); - grantDeParser.deParse(grant); + grant.appendTo(builder, e -> e.accept(expressionDeParser, context)); return builder; } @Override public StringBuilder visit(CreateSchema aThis, S context) { + return aThis.appendTo(builder, element -> element.accept(this, context)); + } + + @Override + public StringBuilder visit(CreateDatabase aThis, S context) { builder.append(aThis.toString()); return builder; } + @Override + public StringBuilder visit(CreateUser createUser, S context) { + builder.append(createUser); + return builder; + } + + @Override + public StringBuilder visit(AlterUser alterUser, S context) { + builder.append(alterUser); + return builder; + } + + @Override + public StringBuilder visit(RenameUser renameUser, S context) { + builder.append(renameUser); + return builder; + } + + @Override + public StringBuilder visit(SetRoleStatement setRole, S context) { + builder.append(setRole); + return builder; + } + + @Override + public StringBuilder visit(SetDefaultRoleStatement setDefaultRole, S context) { + builder.append(setDefaultRole); + return builder; + } + + @Override + public StringBuilder visit(SetPasswordStatement setPassword, S context) { + return setPassword.appendTo(builder, e -> e.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(CreateEvent createEvent, S context) { + builder.append(createEvent); + return builder; + } + @Override public StringBuilder visit(CreateSequence createSequence, S context) { new CreateSequenceDeParser(builder).deParse(createSequence); @@ -445,7 +636,8 @@ public StringBuilder visit(AlterSequence alterSequence, S context) { @Override public StringBuilder visit(CreateFunctionalStatement createFunctionalStatement, S context) { - builder.append(createFunctionalStatement.toString()); + createFunctionalStatement.appendTo(builder, + new TableElementDeParser(builder, expressionDeParser)::deParse); return builder; } @@ -468,8 +660,9 @@ public StringBuilder visit(AlterSession alterSession, S context) { @Override public StringBuilder visit(IfElseStatement ifElseStatement, S context) { - ifElseStatement.appendTo(builder); - return builder; + return ifElseStatement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context), + statement -> statement.accept(this, context)); } @Override @@ -478,6 +671,96 @@ public StringBuilder visit(RenameTableStatement renameTableStatement, S cont return builder; } + @Override + public StringBuilder visit(PragmaStatement pragmaStatement, S context) { + pragmaStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(AssertStatement assertStatement, S context) { + assertStatement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(ExtensionStatement extensionStatement, S context) { + extensionStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(ExportDataStatement exportDataStatement, S context) { + exportDataStatement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context), + select -> select.accept((SelectVisitor) selectDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(AttachStatement attachStatement, S context) { + attachStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(DetachStatement detachStatement, S context) { + detachStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(ConnectStatement connectStatement, S context) { + connectStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(DisconnectStatement disconnectStatement, S context) { + disconnectStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(PrepareStatement prepareStatement, S context) { + prepareStatement.appendTo(builder, statement -> statement.accept(this, context)); + return builder; + } + + @Override + public StringBuilder visit(DeallocateStatement deallocateStatement, S context) { + deallocateStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(CopyStatement copyStatement, S context) { + copyStatement.appendTo(builder, + select -> select.accept((SelectVisitor) selectDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(CreateMacro createMacro, S context) { + createMacro.appendTo(builder, + expression -> expression.accept(expressionDeParser, context), + select -> select.accept((SelectVisitor) selectDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(CreateExtensionRepository createExtensionRepository, S context) { + createExtensionRepository.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(LoadDataStatement loadDataStatement, S context) { + loadDataStatement.appendTo(builder); + return builder; + } + @Override public StringBuilder visit(PurgeStatement purgeStatement, S context) { purgeStatement.appendTo(builder); @@ -518,13 +801,261 @@ public StringBuilder visit(Export export, S context) { @Override public StringBuilder visit(LockStatement lock, S context) { - builder.append(lock.toString()); + lock.appendTo(builder); return builder; } @Override public StringBuilder visit(CreatePolicy createPolicy, S context) { - builder.append(createPolicy.toString()); + new CreatePolicyDeParser(expressionDeParser, builder).deParse(createPolicy); + return builder; + } + + @Override + public StringBuilder visit(CreateRole statement, S context) { + statement.appendTo(builder, e -> e.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(AlterSchema statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterRole statement, S context) { + statement.appendTo(builder, e -> e.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(Revoke statement, S context) { + statement.appendTo(builder, e -> e.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(AlterDefaultPrivileges statement, S context) { + statement.appendTo(builder, e -> e.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(CreateType statement, S context) { + builder.append(statement); return builder; } + + @Override + public StringBuilder visit(AlterType statement, S context) { + builder.append(statement); + return builder; + } + + @Override + public StringBuilder visit(CreateDomain statement, S context) { + statement.appendTo(builder, expression -> expression.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(AlterDomain statement, S context) { + statement.appendTo(builder, expression -> expression.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(CreateExtension statement, S context) { + builder.append(statement); + return builder; + } + + @Override + public StringBuilder visit(AlterExtension statement, S context) { + builder.append(statement); + return builder; + } + + @Override + public StringBuilder visit(CreatePublication statement, S context) { + statement.appendTo(builder, expression -> expression.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(AlterPublication statement, S context) { + statement.appendTo(builder, expression -> expression.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(CreateSubscription statement, S context) { + statement.appendTo(builder, expression -> expression.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(AlterSubscription statement, S context) { + statement.appendTo(builder, expression -> expression.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(OracleBlock block, S context) { + return block.appendTo(builder, expression -> expression.accept(expressionDeParser, context), + statement -> statement.accept(this, context)); + } + + @Override + public StringBuilder visit(OracleAssignment assignment, S context) { + return assignment.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(OracleNullStatement statement, S context) { + return builder.append("NULL"); + } + + @Override + public StringBuilder visit(AlterRelation statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterDatabase statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterTablespaceMove statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateTablespace statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterTablespace statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterPolicy statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(DropPolicy statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateStatistics statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterStatistics statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateForeignDataWrapper statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterForeignDataWrapper statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(CreateServer statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterServer statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(CreateUserMapping statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterUserMapping statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(DropUserMapping statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateAccessMethod statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateTextSearchConfiguration statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterTextSearchConfiguration statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateRule statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context), + action -> action.accept(this, context)); + } + + @Override + public StringBuilder visit(NotifyStatement statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(CreateCollation statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterCollation statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterRoutine statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterTrigger statement, S context) { + return statement.appendTo(builder); + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java new file mode 100644 index 0000000000..d417f4c4a6 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.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.util.deparser; + +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.TableElement; + +/** Deparses table elements while preserving expression visitor customization. */ +public class TableElementDeParser extends AbstractDeParser { + private final ExpressionVisitor expressionVisitor; + + public TableElementDeParser(StringBuilder builder, + ExpressionVisitor expressionVisitor) { + super(builder); + this.expressionVisitor = expressionVisitor; + } + + @Override + public void deParse(TableElement element) { + if (element instanceof Index) { + ((Index) element).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + } else if (element instanceof NamedConstraint) { + ((NamedConstraint) element).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + } else if (element instanceof ColumnDefinition) { + ((ColumnDefinition) element).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + } else { + builder.append(element); + } + } + +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/TableStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/TableStatementDeParser.java index 34aba2a862..8789ab7e23 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/TableStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/TableStatementDeParser.java @@ -13,6 +13,8 @@ import net.sf.jsqlparser.statement.piped.FromQuery; import net.sf.jsqlparser.statement.select.*; +import java.util.List; + /** * @author jxnu-liguobin */ @@ -84,19 +86,39 @@ public StringBuilder visit(LateralSubSelect lateralSubSelect, S context) { @Override public StringBuilder visit(TableStatement tableStatement, S context) { - builder.append("TABLE "); - builder.append(tableStatement.getTable()); + List> withItemsList = tableStatement.getWithItemsList(); + if (withItemsList != null && !withItemsList.isEmpty()) { + SelectDeParser selectDeParser = new SelectDeParser(expressionVisitor, builder); + builder.append("WITH "); + for (int i = 0; i < withItemsList.size(); i++) { + if (i > 0) { + builder.append(", "); + } + withItemsList.get(i).accept((SelectVisitor) selectDeParser, context); + } + builder.append(" "); + } + tableStatement.appendSelectBodyTo(builder); if (tableStatement.getOrderByElements() != null) { new OrderByDeParser(expressionVisitor, builder) .deParse(tableStatement.getOrderByElements()); } + if (tableStatement.getOption() != null) { + builder.append(tableStatement.getOption()); + } if (tableStatement.getLimit() != null) { new LimitDeparser(expressionVisitor, builder).deParse(tableStatement.getLimit()); } if (tableStatement.getOffset() != null) { deparse(tableStatement.getOffset()); } + if (tableStatement.getFetch() != null) { + builder.append(tableStatement.getFetch()); + } + if (tableStatement.getIsolation() != null) { + builder.append(tableStatement.getIsolation()); + } // TODO UNION diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java index d5dcac7f10..8c6b3ab490 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java @@ -11,45 +11,40 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; -import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.OrderByVisitor; -import net.sf.jsqlparser.statement.select.WithItem; import net.sf.jsqlparser.statement.update.Update; -import java.util.Iterator; - public class UpdateDeParser extends AbstractDeParser implements OrderByVisitor { private ExpressionVisitor expressionVisitor = new ExpressionVisitorAdapter<>(); + private SelectDeParser selectDeParser; + public UpdateDeParser() { super(new StringBuilder()); } public UpdateDeParser(ExpressionVisitor expressionVisitor, StringBuilder buffer) { + this(expressionVisitor, null, buffer); + } + + public UpdateDeParser(ExpressionVisitor expressionVisitor, + SelectDeParser selectDeParser, StringBuilder buffer) { super(buffer); this.expressionVisitor = expressionVisitor; + this.selectDeParser = selectDeParser; } @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity", "PMD.ExcessiveMethodLength"}) public void deParse(Update update) { - if (update.getWithItemsList() != null && !update.getWithItemsList().isEmpty()) { - builder.append("WITH "); - for (Iterator> iter = update.getWithItemsList().iterator(); iter - .hasNext();) { - WithItem withItem = iter.next(); - builder.append(withItem); - if (iter.hasNext()) { - builder.append(","); - } - builder.append(" "); - } - } + DmlDeParserSupport sources = + new DmlDeParserSupport(expressionVisitor, selectDeParser, builder); + sources.deparseWithItems(update.getWithItemsList(), null); builder.append("UPDATE "); if (update.getOracleHint() != null) { builder.append(update.getOracleHint()).append(" "); @@ -61,14 +56,12 @@ public void deParse(Update update) { builder.append("IGNORE "); } builder.append(update.getTable()); + deparseForPortionClause(update.getForPortionClause(), expressionVisitor); if (update.getStartJoins() != null) { - for (Join join : update.getStartJoins()) { - if (join.isSimple()) { - builder.append(", ").append(join); - } else { - builder.append(" ").append(join); - } - } + sources.deparseJoins(update.getStartJoins()); + } + if (update.isFromBeforeSet()) { + sources.deparseFrom(update.getFromItem(), update.getJoins()); } builder.append(" SET "); @@ -78,17 +71,8 @@ public void deParse(Update update) { update.getOutputClause().appendTo(builder); } - if (update.getFromItem() != null) { - builder.append(" FROM ").append(update.getFromItem()); - if (update.getJoins() != null) { - for (Join join : update.getJoins()) { - if (join.isSimple()) { - builder.append(", ").append(join); - } else { - builder.append(" ").append(join); - } - } - } + if (!update.isFromBeforeSet()) { + sources.deparseFrom(update.getFromItem(), update.getJoins()); } deparseWhereClause(update); @@ -103,9 +87,11 @@ public void deParse(Update update) { new LimitDeparser(expressionVisitor, builder).deParse(update.getLimit()); } - if (update.getReturningClause() != null) { - update.getReturningClause().appendTo(builder); + if (update.getOption() != null) { + builder.append(update.getOption()); } + + deparseReturningClause(update.getReturningClause(), expressionVisitor); } protected void deparseWhereClause(Update update) { @@ -119,7 +105,6 @@ protected void deparseUpdateSetsClause(Update update) { deparseUpdateSets(update.getUpdateSets(), builder, expressionVisitor); } - public ExpressionVisitor getExpressionVisitor() { return expressionVisitor; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java index eb39481d0b..8c8ffe967a 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java @@ -15,17 +15,30 @@ public class ValuesStatementDeParser extends AbstractDeParser { private final ExpressionVisitor expressionVisitor; + private final SelectDeParser selectDeParser; public ValuesStatementDeParser(ExpressionVisitor expressionVisitor, StringBuilder buffer) { + this(expressionVisitor, null, buffer); + } + + public ValuesStatementDeParser(ExpressionVisitor expressionVisitor, + SelectDeParser selectDeParser, StringBuilder buffer) { super(buffer); this.expressionVisitor = expressionVisitor; + this.selectDeParser = selectDeParser; } @Override public void deParse(Values values) { + deParse(values, null); + } + + public void deParse(Values values, S context) { + new DmlDeParserSupport(expressionVisitor, selectDeParser, builder) + .deparseWithItems(values.getWithItemsList(), context); builder.append("VALUES "); - values.getExpressions().accept(expressionVisitor, null); + values.getExpressions().accept(expressionVisitor, context); if (values.getAlias() != null) { builder.append(" ").append(values.getAlias()); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java index 431e4d7622..3423673c17 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java @@ -45,7 +45,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS */ public static final FeaturesAllowed SELECT = new FeaturesAllowed("SELECT", // select features - Feature.select, + Feature.select, Feature.matchRecognize, Feature.matchRecognizeOptions, Feature.selectGroupBy, Feature.selectHaving, @@ -59,9 +59,11 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS Feature.joinCross, Feature.joinOuter, Feature.joinSemi, + Feature.joinAnti, Feature.joinInner, Feature.joinStraight, Feature.joinApply, + Feature.joinAsOf, Feature.joinWindow, Feature.joinUsingColumns, @@ -84,6 +86,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS Feature.orderBy, Feature.orderByNullOrdering, Feature.tableStatement, + Feature.tableVariable, Feature.function).unmodifyable(); /** @@ -98,19 +101,30 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS * all "CREATE" {@link Feature}'s */ public static final FeaturesAllowed CREATE = new FeaturesAllowed("CREATE", Feature.createIndex, + Feature.createTextSearchConfiguration, + Feature.createRule, + Feature.createCollation, Feature.createSchema, Feature.createSequence, Feature.createTable, + Feature.createForeignDataWrapper, Feature.createServer, Feature.createUserMapping, + Feature.createAccessMethod, Feature.createAccessMethodTable, + Feature.createTablespace, Feature.createTableUnlogged, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, Feature.createTableIfNotExists, Feature.createTableRowMovement, Feature.createTableFromSelect, - Feature.createTrigger, + Feature.createTrigger, Feature.createEventTrigger, Feature.createView).unmodifyable(); /** * all "ALTER" {@link Feature}'s */ public static final FeaturesAllowed ALTER = new FeaturesAllowed("ALTER", Feature.alterTable, Feature.alterSequence, - Feature.alterView, Feature.alterIndex) + Feature.alterTextSearchConfiguration, + Feature.alterCollation, + Feature.alterTablespace, + Feature.alterEventTrigger, + Feature.alterView, Feature.alterIndex, Feature.alterSchema, + Feature.alterForeignDataWrapper, Feature.alterServer, Feature.alterUserMapping) .unmodifyable(); /** * all "DROP" {@link Feature}'s @@ -136,6 +150,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS * all {@link Feature}' for SQL UPDATE including {@link #SELECT} */ public static final FeaturesAllowed UPDATE = new FeaturesAllowed("UPDATE", Feature.update, + Feature.forPortion, Feature.updateJoins, Feature.updateFrom, Feature.updateLimit, Feature.updateOrderBy, Feature.updateReturning, Feature.updateUseSelect) @@ -144,7 +159,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS * all {@link Feature}' for SQL UPDATE including {@link #SELECT} */ public static final FeaturesAllowed DELETE = - new FeaturesAllowed("DELETE", Feature.delete, Feature.deleteJoin, + new FeaturesAllowed("DELETE", Feature.delete, Feature.forPortion, Feature.deleteJoin, Feature.deleteLimit, Feature.deleteOrderBy, Feature.deleteTables, Feature.deleteReturningExpressionList, Feature.truncate) @@ -153,7 +168,8 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS * all DML {@link Feature}'s */ public static final FeaturesAllowed DML = - new FeaturesAllowed("DML").add(SELECT, INSERT, UPDATE, DELETE, MERGE) + new FeaturesAllowed("DML", Feature.notifyStatement) + .add(SELECT, INSERT, UPDATE, DELETE, MERGE) .unmodifyable(); /** * all DDL {@link Feature}'s diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java index 742e84b976..f4ca890270 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java @@ -26,11 +26,10 @@ public enum MariaDbVersion implements Version { V10_5_4("10.5.4", EnumSet.of(// supported if used with jdbc Feature.jdbcParameter, - Feature.jdbcNamedParameter, - // expressions - Feature.exprLike, - // https://mariadb.com/kb/en/select/ - Feature.select, + Feature.jdbcNamedParameter, // expressions + Feature.exprLike, // https://mariadb.com/kb/en/select/ + Feature.select, // https://mariadb.com/kb/en/selectinto/ + Feature.selectInto, Feature.selectGroupBy, Feature.function, Feature.selectHaving, Feature.limit, Feature.limitOffset, Feature.offset, Feature.offsetParam, @@ -49,12 +48,9 @@ public enum MariaDbVersion implements Version { // https://mariadb.com/kb/en/select/#distinct Feature.distinct, - Feature.setOperation, - // https://mariadb.com/kb/en/union/ - Feature.setOperationUnion, - // https://mariadb.com/kb/en/intersect/ - Feature.setOperationIntersect, - // https://mariadb.com/kb/en/except/ + Feature.setOperation, // https://mariadb.com/kb/en/union/ + Feature.setOperationUnion, // https://mariadb.com/kb/en/intersect/ + Feature.setOperationIntersect, // https://mariadb.com/kb/en/except/ Feature.setOperationExcept, // https://mariadb.com/kb/en/common-table-expressions/ @@ -71,7 +67,8 @@ public enum MariaDbVersion implements Version { Feature.insertReturningExpressionList, // https://mariadb.com/kb/en/update/ - Feature.update, + Feature.update, // https://mariadb.com/kb/en/application-time-periods/ + Feature.forPortion, Feature.updateJoins, Feature.updateOrderBy, Feature.updateLimit, @@ -83,21 +80,23 @@ public enum MariaDbVersion implements Version { // https://mariadb.com/kb/en/truncate-table/ Feature.truncate, + // https://mariadb.com/kb/en/table-statements/ + Feature.tableMaintenance, + + // https://mariadb.com/kb/en/account-management-sql-statements/ + Feature.createRole, Feature.alterUser, Feature.renameUser, Feature.setRole, + Feature.setDefaultRole, Feature.setPassword, + // https://mariadb.com/kb/en/call/ Feature.execute, Feature.executeCall, // https://mariadb.com/kb/en/drop/ - Feature.drop, - // https://mariadb.com/kb/en/drop-index/ - Feature.dropIndex, - // https://mariadb.com/kb/en/drop-table/ - Feature.dropTable, - // https://mariadb.com/kb/en/drop-database/ + Feature.drop, // https://mariadb.com/kb/en/drop-index/ + Feature.dropIndex, // https://mariadb.com/kb/en/drop-table/ + Feature.dropTable, // https://mariadb.com/kb/en/drop-database/ // SCHEMA = DATABASE - Feature.dropSchema, - // https://mariadb.com/kb/en/drop-view/ - Feature.dropView, - // https://mariadb.com/kb/en/drop-sequence/ + Feature.dropSchema, // https://mariadb.com/kb/en/drop-view/ + Feature.dropView, // https://mariadb.com/kb/en/drop-sequence/ Feature.dropSequence, Feature.dropTableIfExists, Feature.dropIndexIfExists, Feature.dropViewIfExists, Feature.dropSchemaIfExists, Feature.dropSequenceIfExists, @@ -106,12 +105,9 @@ public enum MariaDbVersion implements Version { Feature.upsert, // https://mariadb.com/kb/en/alter/ - Feature.alterTable, - // https://mariadb.com/kb/en/alter-sequence/ - Feature.alterSequence, - // https://mariadb.com/kb/en/alter-view/ - Feature.alterView, - // https://mariadb.com/kb/en/create-view/ + Feature.alterTable, // https://mariadb.com/kb/en/alter-sequence/ + Feature.alterSequence, // https://mariadb.com/kb/en/alter-view/ + Feature.alterView, // https://mariadb.com/kb/en/create-view/ Feature.createView, Feature.createOrReplaceView, Feature.createViewWithComment, @@ -119,37 +115,35 @@ public enum MariaDbVersion implements Version { // https://mariadb.com/kb/en/create-table/ Feature.createTable, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, - Feature.createTableFromSelect, Feature.createTableIfNotExists, - // https://mariadb.com/kb/en/create-index/ - Feature.createIndex, - // https://mariadb.com/kb/en/create-sequence/ - Feature.createSequence, - // https://mariadb.com/kb/en/create-database/ - Feature.createSchema, - // https://mariadb.com/kb/en/create-trigger/ + Feature.createTableFromSelect, Feature.createTableIfNotExists, // https://mariadb.com/kb/en/create-index/ + Feature.createIndex, // https://mariadb.com/kb/en/create-sequence/ + Feature.createSequence, // https://mariadb.com/kb/en/create-database/ + Feature.createSchema, // https://mariadb.com/kb/en/create-trigger/ Feature.createTrigger, + // https://mariadb.com/kb/en/create-database/ + // https://mariadb.com/kb/en/alter-database/ + Feature.createDatabase, Feature.alterDatabase, + + // https://mariadb.com/kb/en/alter-function/ + // https://mariadb.com/kb/en/alter-procedure/ + Feature.alterFunction, Feature.alterProcedure, + // https://mariadb.com/kb/en/describe/ - Feature.describe, - // https://mariadb.com/kb/en/explain/ - Feature.explain, - // https://mariadb.com/kb/en/show/ - Feature.show, - // https://mariadb.com/kb/en/show-tables/ - Feature.showTables, - // https://mariadb.com/kb/en/show-columns/ - Feature.showColumns, - // https://mariadb.com/kb/en/show-index/ - Feature.showIndex, - // https://mariadb.com/kb/en/use/ - Feature.use, - // https://mariadb.com/kb/en/grant/ - Feature.grant, - // https://mariadb.com/kb/en/commit/ - Feature.commit, - // https://mariadb.com/kb/en/optimizer-hints/ + Feature.describe, // https://mariadb.com/kb/en/explain/ + Feature.explain, // https://mariadb.com/kb/en/show/ + Feature.show, // https://mariadb.com/kb/en/show-tables/ + Feature.showTables, // https://mariadb.com/kb/en/show-columns/ + Feature.showColumns, // https://mariadb.com/kb/en/show-index/ + Feature.showIndex, // https://mariadb.com/kb/en/show-table-status/ + Feature.showTableStatus, // https://mariadb.com/kb/en/show-create-table/ + Feature.showCreate, // https://mariadb.com/kb/en/use/ + Feature.use, // https://mariadb.com/kb/en/grant/ + Feature.grant, // https://mariadb.com/kb/en/revoke/ + Feature.revoke, // https://mariadb.com/kb/en/commit/ + Feature.commit, // https://mariadb.com/kb/en/optimizer-hints/ Feature.mySqlHintStraightJoin, - Feature.mysqlCalcFoundRows, + Feature.mysqlSelectModifiers, Feature.mysqlCalcFoundRows, Feature.mysqlSqlCacheFlag)), ORACLE_MODE("oracle_mode", V10_5_4.copy().add(Feature.selectUnique).getFeatures()); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java index cd6c2c038b..17a1eae4d7 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java @@ -24,14 +24,12 @@ */ public enum MySqlVersion implements Version { V8_0("8.0", - EnumSet.of( - // supported if used with jdbc + EnumSet.of(// supported if used with jdbc Feature.jdbcParameter, - Feature.jdbcNamedParameter, - // expressions - Feature.exprLike, - // https://dev.mysql.com/doc/refman/8.0/en/select.html - Feature.select, + Feature.jdbcNamedParameter, // expressions + Feature.exprLike, // https://dev.mysql.com/doc/refman/8.0/en/select.html + Feature.select, // https://dev.mysql.com/doc/refman/8.0/en/select-into.html + Feature.selectInto, Feature.selectGroupBy, Feature.selectHaving, Feature.limit, Feature.limitOffset, Feature.offset, Feature.offsetParam, Feature.orderBy, @@ -42,8 +40,7 @@ public enum MySqlVersion implements Version { Feature.selectForShare, Feature.distinct, - Feature.setOperation, - // https://dev.mysql.com/doc/refman/8.0/en/union.html + Feature.setOperation, // https://dev.mysql.com/doc/refman/8.0/en/union.html Feature.setOperationUnion, // https://dev.mysql.com/doc/refman/8.0/en/with.html#common-table-expressions @@ -79,64 +76,69 @@ public enum MySqlVersion implements Version { // https://dev.mysql.com/doc/refman/8.0/en/truncate-table.html Feature.truncate, + // https://dev.mysql.com/doc/refman/8.0/en/table-maintenance-statements.html + Feature.tableMaintenance, + + // https://dev.mysql.com/doc/refman/8.0/en/account-management-statements.html + Feature.createRole, Feature.alterUser, Feature.renameUser, Feature.setRole, + Feature.setDefaultRole, Feature.setPassword, + // https://dev.mysql.com/doc/refman/8.0/en/call.html Feature.execute, Feature.executeCall, - Feature.drop, - // https://dev.mysql.com/doc/refman/8.0/en/drop-table.html - Feature.dropTable, - // https://dev.mysql.com/doc/refman/8.0/en/drop-index.html - Feature.dropIndex, - // https://dev.mysql.com/doc/refman/8.0/en/drop-view.html - Feature.dropView, - // https://dev.mysql.com/doc/refman/8.0/en/drop-database.html + Feature.drop, // https://dev.mysql.com/doc/refman/8.0/en/drop-table.html + Feature.dropTable, // https://dev.mysql.com/doc/refman/8.0/en/drop-index.html + Feature.dropIndex, // https://dev.mysql.com/doc/refman/8.0/en/drop-view.html + Feature.dropView, // https://dev.mysql.com/doc/refman/8.0/en/drop-database.html Feature.dropSchema, Feature.dropTableIfExists, Feature.dropViewIfExists, Feature.dropSchemaIfExists, Feature.dropSequenceIfExists, // https://dev.mysql.com/doc/refman/8.0/en/alter-table.html - Feature.alterTable, - // https://dev.mysql.com/doc/refman/8.0/en/alter-view.html + Feature.alterTable, // https://dev.mysql.com/doc/refman/8.0/en/alter-view.html Feature.alterView, + // https://dev.mysql.com/doc/refman/8.0/en/create-tablespace.html + Feature.createTablespace, // https://dev.mysql.com/doc/refman/8.0/en/alter-tablespace.html + Feature.alterTablespace, + + // https://dev.mysql.com/doc/refman/8.0/en/create-database.html + // https://dev.mysql.com/doc/refman/8.0/en/alter-database.html + Feature.createDatabase, Feature.alterDatabase, + + // https://dev.mysql.com/doc/refman/8.0/en/alter-function.html + // https://dev.mysql.com/doc/refman/8.0/en/alter-procedure.html + Feature.alterFunction, Feature.alterProcedure, + // https://dev.mysql.com/doc/refman/8.0/en/create-database.html - Feature.createSchema, - // https://dev.mysql.com/doc/refman/8.0/en/create-view.html + Feature.createSchema, // https://dev.mysql.com/doc/refman/8.0/en/create-view.html Feature.createView, Feature.createViewWithComment, - Feature.createOrReplaceView, - // https://dev.mysql.com/doc/refman/8.0/en/create-table.html + Feature.createOrReplaceView, // https://dev.mysql.com/doc/refman/8.0/en/create-table.html Feature.createTable, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, - Feature.createTableFromSelect, Feature.createTableIfNotExists, - // https://dev.mysql.com/doc/refman/8.0/en/create-index.html - Feature.createIndex, - // https://dev.mysql.com/doc/refman/8.0/en/create-trigger.html + Feature.createTableFromSelect, Feature.createTableIfNotExists, // https://dev.mysql.com/doc/refman/8.0/en/create-index.html + Feature.createIndex, // https://dev.mysql.com/doc/refman/8.0/en/create-trigger.html Feature.createTrigger, // https://dev.mysql.com/doc/refman/8.0/en/describe.html Feature.describe, - Feature.desc, - // https://dev.mysql.com/doc/refman/8.0/en/explain.html - Feature.explain, - // https://dev.mysql.com/doc/refman/8.0/en/show.html - Feature.show, - // https://dev.mysql.com/doc/refman/8.0/en/show-tables.html - Feature.showTables, - // https://dev.mysql.com/doc/refman/8.0/en/show-columns.html - Feature.showColumns, - // https://dev.mysql.com/doc/refman/8.0/en/show-index.html - Feature.showIndex, - // https://dev.mysql.com/doc/refman/8.0/en/grant.html - Feature.grant, - // https://dev.mysql.com/doc/refman/8.0/en/use.html - Feature.use, - // https://dev.mysql.com/doc/refman/8.0/en/commit.html - Feature.commit, - // + Feature.desc, // https://dev.mysql.com/doc/refman/8.0/en/explain.html + Feature.explain, // https://dev.mysql.com/doc/refman/8.0/en/show.html + Feature.show, // https://dev.mysql.com/doc/refman/8.0/en/show-tables.html + Feature.showTables, // https://dev.mysql.com/doc/refman/8.0/en/show-columns.html + Feature.showColumns, // https://dev.mysql.com/doc/refman/8.0/en/show-index.html + Feature.showIndex, // https://dev.mysql.com/doc/refman/8.0/en/show-table-status.html + Feature.showTableStatus, // https://dev.mysql.com/doc/refman/8.0/en/show-create-table.html + Feature.showCreate, // https://dev.mysql.com/doc/refman/8.0/en/grant.html + Feature.grant, // https://dev.mysql.com/doc/refman/8.0/en/revoke.html + Feature.revoke, // https://dev.mysql.com/doc/refman/8.0/en/use.html + Feature.use, // https://dev.mysql.com/doc/refman/8.0/en/commit.html + Feature.commit, Feature.startTransaction, Feature.releaseSavepoint, + Feature.transactionChain, Feature.transactionConsistentSnapshot, // Feature.mySqlHintStraightJoin, Feature.mysqlSqlCacheFlag, - Feature.mysqlCalcFoundRows)); + Feature.mysqlSelectModifiers, Feature.mysqlCalcFoundRows)); private Set features; private String versionString; diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/OracleVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/OracleVersion.java index 37b1938207..8d94ab72c1 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/OracleVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/OracleVersion.java @@ -24,20 +24,15 @@ */ public enum OracleVersion implements Version { V19C("19c", - EnumSet.of( - // supported if used with jdbc + EnumSet.of(// supported if used with jdbc Feature.jdbcParameter, - Feature.jdbcNamedParameter, - // expressions - Feature.exprLike, - // common features + Feature.jdbcNamedParameter, // expressions + Feature.exprLike, // common features // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html - Feature.select, - // https://www.oracletutorial.com/oracle-basics/oracle-group-by/ - Feature.selectGroupBy, Feature.function, - // https://www.oracletutorial.com/oracle-basics/oracle-grouping-sets/ - Feature.selectGroupByGroupingSets, - // https://www.oracletutorial.com/oracle-basics/oracle-having/ + Feature.matchRecognize, + Feature.select, // https://www.oracletutorial.com/oracle-basics/oracle-group-by/ + Feature.selectGroupBy, Feature.function, // https://www.oracletutorial.com/oracle-basics/oracle-grouping-sets/ + Feature.selectGroupByGroupingSets, // https://www.oracletutorial.com/oracle-basics/oracle-having/ Feature.selectHaving, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html @@ -91,8 +86,7 @@ public enum OracleVersion implements Version { // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/INSERT.html Feature.insert, Feature.insertValues, - Feature.values, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/INSERT.html + Feature.values, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/INSERT.html // see "single_table_insert" Feature.insertFromSelect, @@ -108,38 +102,25 @@ public enum OracleVersion implements Version { // https://www.oracletutorial.com/oracle-basics/oracle-truncate-table/ Feature.truncate, - Feature.drop, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-TABLE.html - Feature.dropTable, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-INDEX.html - Feature.dropIndex, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-VIEW.html - Feature.dropView, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-SEQUENCE.html + Feature.drop, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-TABLE.html + Feature.dropTable, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-INDEX.html + Feature.dropIndex, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-VIEW.html + Feature.dropView, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-SEQUENCE.html Feature.dropSequence, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ALTER-TABLE.html - Feature.alterTable, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ALTER-SEQUENCE.html - Feature.alterSequence, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/EXECUTE-IMMEDIATE-statement.html - Feature.executeStatementImmediate, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-VIEW.html + Feature.alterTable, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ALTER-SEQUENCE.html + Feature.alterSequence, // https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/EXECUTE-IMMEDIATE-statement.html + Feature.executeStatementImmediate, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-VIEW.html Feature.createView, - Feature.createViewForce, Feature.createOrReplaceView, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-MATERIALIZED-VIEW.htm - Feature.createViewMaterialized, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TABLE.html + Feature.createViewForce, Feature.createOrReplaceView, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-MATERIALIZED-VIEW.htm + Feature.createViewMaterialized, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TABLE.html Feature.createTable, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, - Feature.createTableFromSelect, Feature.createTableRowMovement, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-INDEX.html - Feature.createIndex, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SEQUENCE.html - Feature.createSequence, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TRIGGER.html - Feature.createTrigger, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SCHEMA.html + Feature.createTableFromSelect, Feature.createTableRowMovement, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-INDEX.html + Feature.createIndex, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SEQUENCE.html + Feature.createSequence, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TRIGGER.html + Feature.createTrigger, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SCHEMA.html Feature.createSchema, Feature.commit, @@ -158,7 +139,7 @@ public enum OracleVersion implements Version { Feature.merge, Feature.createFunction, Feature.createProcedure, Feature.functionalStatement, - Feature.block, + Feature.block, Feature.oracleBlock, Feature.execute, Feature.declare, // special oracle features diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java index 386274fa1e..b0d8115d91 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java @@ -24,19 +24,17 @@ */ public enum PostgresqlVersion implements Version { V10("10", - EnumSet.of( - // supported if used with jdbc + EnumSet.of(// supported if used with jdbc Feature.jdbcParameter, - Feature.jdbcNamedParameter, - // expressions + Feature.jdbcNamedParameter, // expressions + Feature.doStatement, Feature.exprLike, - Feature.exprSimilarTo, - // https://www.postgresql.org/docs/current/sql-select.html + Feature.exprSimilarTo, // https://www.postgresql.org/docs/current/sql-select.html Feature.select, + Feature.tableStatement, Feature.selectGroupBy, Feature.function, Feature.tableFunction, Feature.lateralSubSelect, - Feature.selectHaving, - // https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-GROUPING-SETS + Feature.selectHaving, // https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-GROUPING-SETS Feature.selectGroupByGroupingSets, // https://www.postgresql.org/docs/current/sql-select.html#join_type @@ -93,24 +91,63 @@ public enum PostgresqlVersion implements Version { Feature.commentOnTable, Feature.commentOnColumn, Feature.commentOnView, + Feature.commentOnIndex, Feature.commentOnSchema, Feature.commentOnSequence, + Feature.commentOnDomain, Feature.commentOnType, + Feature.commentOnMaterializedView, + Feature.commentOnFunction, Feature.commentOnConstraint, + Feature.commentOnTrigger, Feature.commentOnPolicy, Feature.commentOnRule, + Feature.commentOnProcedure, Feature.commentOnRoutine, + Feature.commentOnAggregate, Feature.commentOnCast, + Feature.commentOnCollation, Feature.commentOnForeignTable, + Feature.commentOnStatistics, Feature.commentOnDatabase, Feature.commentOnRole, + Feature.commentOnTablespace, Feature.commentOnAccessMethod, + Feature.commentOnExtension, Feature.commentOnForeignDataWrapper, + Feature.commentOnLanguage, Feature.commentOnServer, + Feature.commentOnEventTrigger, Feature.commentOnPublication, + Feature.commentOnSubscription, + Feature.commentOnOperator, Feature.commentOnOperatorClass, + Feature.commentOnOperatorFamily, Feature.commentOnConversion, + Feature.commentOnLargeObject, Feature.commentOnTransform, + Feature.commentOnTextSearchConfiguration, + Feature.commentOnTextSearchDictionary, Feature.commentOnTextSearchParser, + Feature.commentOnTextSearchTemplate, // https://www.postgresql.org/docs/current/sql-createsequence.html Feature.createSequence, - // https://www.postgresql.org/docs/current/sql-altersequence.html - Feature.alterSequence, - // https://www.postgresql.org/docs/current/sql-createschema.html - Feature.createSchema, - // https://www.postgresql.org/docs/current/sql-createindex.html - Feature.createIndex, - // https://www.postgresql.org/docs/current/sql-createtable.html + Feature.createAccessMethod, + Feature.createTablespace, Feature.alterTablespace, + Feature.createForeignDataWrapper, Feature.alterForeignDataWrapper, + Feature.createServer, Feature.alterServer, Feature.createUserMapping, + Feature.alterUserMapping, + Feature.createTextSearchConfiguration, Feature.alterTextSearchConfiguration, + Feature.createRule, Feature.notifyStatement, + Feature.createCollation, Feature.alterCollation, + Feature.createPublication, Feature.alterPublication, + Feature.createSubscription, Feature.alterSubscription, + Feature.createType, Feature.alterType, + Feature.createDomain, Feature.alterDomain, + Feature.createExtension, Feature.alterExtension, // https://www.postgresql.org/docs/current/sql-altersequence.html + Feature.alterSequence, // https://www.postgresql.org/docs/current/sql-createschema.html + Feature.createSchema, Feature.alterSchema, // https://www.postgresql.org/docs/current/sql-createindex.html + Feature.createIndex, // https://www.postgresql.org/docs/current/sql-createtable.html Feature.createTable, Feature.createTableUnlogged, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, - Feature.createTableFromSelect, Feature.createTableIfNotExists, - // https://www.postgresql.org/docs/current/sql-createview.html + Feature.createTableFromSelect, Feature.createTableIfNotExists, // https://www.postgresql.org/docs/current/sql-createview.html Feature.createView, Feature.createViewTemporary, Feature.createOrReplaceView, - // https://www.postgresql.org/docs/current/sql-alterview.html + Feature.createViewMaterialized, // https://www.postgresql.org/docs/current/sql-alterview.html // Feature.alterView, + // https://www.postgresql.org/docs/current/sql-createdatabase.html + // https://www.postgresql.org/docs/current/sql-alterdatabase.html + Feature.createDatabase, Feature.alterDatabase, + + // https://www.postgresql.org/docs/current/sql-alterfunction.html + // https://www.postgresql.org/docs/current/sql-alterprocedure.html + // https://www.postgresql.org/docs/current/sql-alterroutine.html + // https://www.postgresql.org/docs/current/sql-altertrigger.html + Feature.alterFunction, Feature.alterProcedure, Feature.alterRoutine, + Feature.alterTrigger, + // https://www.postgresql.org/docs/16/sql-refreshmaterializedview.html Feature.refreshMaterializedView, Feature.refreshMaterializedWithNoDataView, @@ -122,47 +159,48 @@ public enum PostgresqlVersion implements Version { Feature.values, Feature.insertFromSelect, Feature.insertReturningAll, - Feature.insertReturningExpressionList, - // https://www.postgresql.org/docs/current/sql-update.html + Feature.insertReturningExpressionList, // https://www.postgresql.org/docs/current/sql-update.html Feature.update, - Feature.updateReturning, - // https://www.postgresql.org/docs/current/sql-delete.html + Feature.updateReturning, // https://www.postgresql.org/docs/current/sql-delete.html Feature.delete, - Feature.deleteReturningExpressionList, - // https://www.postgresql.org/docs/current/sql-truncate.html + Feature.deleteReturningExpressionList, // https://www.postgresql.org/docs/current/sql-truncate.html Feature.truncate, // https://www.postgresql.org/docs/current/sql-call.html Feature.execute, Feature.executeCall, - Feature.drop, - // https://www.postgresql.org/docs/current/sql-droptable.html - Feature.dropTable, - // https://www.postgresql.org/docs/current/sql-dropindex.html - Feature.dropIndex, - // https://www.postgresql.org/docs/current/sql-dropview.html - Feature.dropView, - // https://www.postgresql.org/docs/current/sql-dropschema.html - Feature.dropSchema, - // https://www.postgresql.org/docs/current/sql-dropsequence.html + Feature.drop, // https://www.postgresql.org/docs/current/sql-droptable.html + Feature.dropTable, // https://www.postgresql.org/docs/current/sql-dropindex.html + Feature.dropIndex, // https://www.postgresql.org/docs/current/sql-dropview.html + Feature.dropView, // https://www.postgresql.org/docs/current/sql-dropschema.html + Feature.dropSchema, // https://www.postgresql.org/docs/current/sql-dropsequence.html Feature.dropSequence, Feature.dropTableIfExists, Feature.dropIndexIfExists, Feature.dropViewIfExists, - Feature.dropSchemaIfExists, Feature.dropSequenceIfExists, + Feature.dropSchemaIfExists, Feature.dropSequenceIfExists, // https://www.postgresql.org/docs/current/sql-reassign-owned.html + Feature.reassignOwned, // https://www.postgresql.org/docs/current/sql-altertable.html - Feature.alterTable, - // https://www.postgresql.org/docs/current/using-explain.html - Feature.explain, - // https://www.postgresql.org/docs/current/sql-grant.html + Feature.alterTable, // https://www.postgresql.org/docs/current/using-explain.html + Feature.explain, // https://www.postgresql.org/docs/current/sql-grant.html Feature.grant, - // https://www.postgresql.org/docs/current/sql-set.html - Feature.set, - // https://www.postgresql.org/docs/current/sql-reset.html - Feature.reset, - // https://www.postgresql.org/docs/current/sql-commit.html - Feature.commit)), V11("11", V10.copy().getFeatures()), V12("12", - V11.copy().getFeatures()), V13("13", - V12.copy().getFeatures()), V14("14", V13.copy().getFeatures()); + Feature.revoke, Feature.createRole, Feature.alterRole, + Feature.alterDefaultPrivileges, Feature.createTrigger, // https://www.postgresql.org/docs/current/sql-createeventtrigger.html + Feature.createEventTrigger, Feature.alterEventTrigger, // https://www.postgresql.org/docs/current/sql-reindex.html + Feature.reindex, // https://www.postgresql.org/docs/current/sql-security-label.html + Feature.securityLabel, // https://www.postgresql.org/docs/current/sql-set.html + Feature.set, Feature.setAssignmentTo, // https://www.postgresql.org/docs/current/sql-reset.html + Feature.reset, // https://www.postgresql.org/docs/current/sql-commit.html + Feature.commit, Feature.startTransaction, Feature.releaseSavepoint, + Feature.transactionIsolationLevel, Feature.transactionDeferrable)), V11("11", + V10.copy().getFeatures()), V12("12", + V11.copy() + .add(Feature.createAccessMethodTable, + Feature.reindexConcurrently, + Feature.transactionChain) + .getFeatures()), V13( + "13", + V12.copy().getFeatures()), V14("14", + V13.copy().getFeatures()); private Set features; private String versionString; diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java index a59e787162..e7c11c9a7b 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java @@ -22,26 +22,20 @@ */ public enum SqlServerVersion implements Version { V2019("2019", - EnumSet.of( - // supported if used with jdbc + EnumSet.of(// supported if used with jdbc Feature.jdbcParameter, - Feature.jdbcNamedParameter, - // expressions - Feature.exprLike, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?view=sql-server-ver15 + Feature.jdbcNamedParameter, // expressions + Feature.exprLike, // https://docs.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?view=sql-server-ver15 Feature.select, Feature.selectInto, Feature.withItem, - Feature.selectGroupBy, Feature.function, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/from-transact-sql?view=sql-server-ver15 + Feature.selectGroupBy, Feature.function, // https://docs.microsoft.com/en-us/sql/t-sql/queries/from-transact-sql?view=sql-server-ver15 // see user_defined_function Feature.tableFunction, Feature.selectHaving, Feature.orderBy, Feature.distinct, - Feature.withItem, Feature.withItemRecursive, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?view=sql-server-ver15 - Feature.top, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql?view=sql-server-ver15 + Feature.withItem, Feature.withItemRecursive, // https://docs.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?view=sql-server-ver15 + Feature.top, // https://docs.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql?view=sql-server-ver15 Feature.offset, Feature.offsetParam, Feature.fetch, Feature.fetchFirst, Feature.fetchNext, @@ -66,27 +60,21 @@ public enum SqlServerVersion implements Version { // https://docs.microsoft.com/en-us/sql/t-sql/statements/insert-transact-sql?view=sql-server-ver15 Feature.insert, + Feature.insertBulk, Feature.insertValues, - Feature.insertFromSelect, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-ver15 + Feature.insertFromSelect, // https://docs.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-ver15 Feature.update, Feature.updateFrom, // https://docs.microsoft.com/en-us/sql/t-sql/statements/delete-transact-sql?view=sql-server-ver15 - Feature.delete, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql?view=sql-server-ver15 + Feature.delete, // https://docs.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql?view=sql-server-ver15 Feature.truncate, - Feature.drop, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-table-transact-sql?view=sql-server-ver15 - Feature.dropTable, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-index-transact-sql?view=sql-server-ver15 - Feature.dropIndex, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-view-transact-sql?view=sql-server-ver15 - Feature.dropView, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-schema-transact-sql?view=sql-server-ver15 - Feature.dropSchema, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-sequence-transact-sql?view=sql-server-ver15 + Feature.drop, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-table-transact-sql?view=sql-server-ver15 + Feature.dropTable, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-index-transact-sql?view=sql-server-ver15 + Feature.dropIndex, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-view-transact-sql?view=sql-server-ver15 + Feature.dropView, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-schema-transact-sql?view=sql-server-ver15 + Feature.dropSchema, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-sequence-transact-sql?view=sql-server-ver15 Feature.dropSequence, Feature.dropTableIfExists, Feature.dropIndexIfExists, Feature.dropViewIfExists, Feature.dropSchemaIfExists, Feature.dropSequenceIfExists, @@ -96,44 +84,32 @@ public enum SqlServerVersion implements Version { Feature.executeExec, Feature.executeExecute, // https://docs.microsoft.com/en-us/sql/t-sql/language-elements/set-local-variable-transact-sql?view=sql-server-ver15 - Feature.set, + Feature.set, Feature.sqlServerSetOptions, // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-table-transact-sql?view=sql-server-ver15 - Feature.alterTable, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-sequence-transact-sql?view=sql-server-ver15 - Feature.alterSequence, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-view-transact-sql?view=sql-server-ver15 - Feature.alterView, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-index-transact-sql?view=sql-server-ver15 - Feature.alterIndex, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-index-transact-sql?view=sql-server-ver15 - Feature.createIndex, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-sequence-transact-sql?view=sql-server-ver15 - Feature.createSequence, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql?view=sql-server-ver15 + Feature.alterTable, // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-sequence-transact-sql?view=sql-server-ver15 + Feature.alterSequence, // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-view-transact-sql?view=sql-server-ver15 + Feature.alterView, // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-index-transact-sql?view=sql-server-ver15 + Feature.alterIndex, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-index-transact-sql?view=sql-server-ver15 + Feature.createIndex, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-sequence-transact-sql?view=sql-server-ver15 + Feature.createSequence, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql?view=sql-server-ver15 Feature.createTable, Feature.createTableTableOptionStrings, - Feature.createTableFromSelect, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver15 + Feature.createTableFromSelect, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver15 // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-procedure-transact-sql?view=sql-server-ver15 Feature.functionalStatement, Feature.createProcedure, Feature.createFunction, + Feature.alterFunction, Feature.alterProcedure, Feature.createOrAlterRoutine, Feature.block, Feature.declare, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-schema-transact-sql?view=sql-server-ver15 - Feature.createSchema, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql?view=sql-server-ver15 - Feature.createView, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver15 - Feature.createTrigger, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver15 - Feature.merge, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver15 - Feature.grant, - // https://docs.microsoft.com/en-us/sql/t-sql/language-elements/commit-transaction-transact-sql?view=sql-server-ver15 - Feature.commit, - // special sql-server features + Feature.tableVariable, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-schema-transact-sql?view=sql-server-ver15 + Feature.createSchema, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql?view=sql-server-ver15 + Feature.createView, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver15 + Feature.createTrigger, // https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver15 + Feature.merge, // https://docs.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver15 + Feature.grant, // https://docs.microsoft.com/en-us/sql/t-sql/language-elements/commit-transaction-transact-sql?view=sql-server-ver15 + Feature.commit, // special sql-server features // https://docs.microsoft.com/en-us/sql/relational-databases/xml/for-xml-sql-server?view=sql-server-ver15 Feature.selectForXmlPath, - Feature.use, Feature.allowSquareBracketQuotation, // + Feature.use, Feature.setIdentityInsert, Feature.allowSquareBracketQuotation, // Feature.pivot, Feature.unpivot, Feature.pivotXml, Feature.selectGroupByGroupingSets)); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/AbstractValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/AbstractValidator.java index 9e810ca822..4955783764 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/AbstractValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/AbstractValidator.java @@ -9,7 +9,11 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.ForPortionClause; import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.update.UpdateSet; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectVisitor; import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.OrderByElement; @@ -129,6 +133,15 @@ protected > void validateOptionalList(List elementL } } + protected void validateOptionalForPortionClause(ForPortionClause clause) { + if (clause != null) { + for (ValidationCapability capability : getCapabilities()) { + validateFeature(capability, Feature.forPortion); + } + clause.accept(getValidator(ExpressionValidator.class), null); + } + } + protected void validateOptionalExpression(Expression expression) { validateOptional(expression, e -> e.accept(getValidator(ExpressionValidator.class), null)); } @@ -137,6 +150,25 @@ protected void validateOptionalExpression(Expression expression, ExpressionValid validateOptional(expression, e -> e.accept(v, null)); } + protected void validateOptionalUpdateSets(List updateSets) { + if (updateSets != null) { + for (UpdateSet updateSet : updateSets) { + validateOptionalExpressions(updateSet.getColumns()); + if (updateSet.getValues() != null) { + for (Expression value : updateSet.getValues()) { + if (value instanceof Select) { + ((Select) value).accept( + (SelectVisitor) getValidator(SelectValidator.class), + null); + } else { + validateOptionalExpression(value); + } + } + } + } + } + } + protected void validateOptionalExpressions(List expressions) { validateOptionalList(expressions, () -> getValidator(ExpressionValidator.class), (o, v) -> o.accept(v, null)); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterSequenceValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterSequenceValidator.java index 80d9e73a12..3e3830c599 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterSequenceValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterSequenceValidator.java @@ -21,6 +21,9 @@ public class AlterSequenceValidator extends AbstractValidator { @Override public void validate(AlterSequence statement) { + if (statement.getSequence().getOwnership() != null) { + validateOptionalExpression(statement.getSequence().getOwnership().getColumn()); + } validateFeatureAndName(Feature.alterSequence, NamedObject.sequence, statement.getSequence().getFullyQualifiedName()); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java index 4db652b960..ece47d337a 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java @@ -12,12 +12,25 @@ import static java.util.stream.Collectors.toList; import java.util.EnumSet; +import java.util.List; import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.statement.alter.AlterExpression; import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDropNotNull; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull; import net.sf.jsqlparser.statement.alter.AlterOperation; +import net.sf.jsqlparser.statement.alter.ColumnPosition; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.KeyColumnSource; +import net.sf.jsqlparser.statement.create.table.KeyElement; +import net.sf.jsqlparser.statement.create.table.ConstraintUsingIndex; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.TableElement; +import net.sf.jsqlparser.util.TableDefinitionTraversal; import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.ValidationUtil; import net.sf.jsqlparser.util.validation.metadata.NamedObject; @@ -37,6 +50,9 @@ public void validate(Alter alter) { } public void validate(Alter alter, AlterExpression e) { + TableDefinitionTraversal.visit(e, this::validateOptionalExpression, + this::validateOptionalFromItem); + TableElement definition = e.getActiveTableDefinition(); for (ValidationCapability c : getCapabilities()) { validateOptionalColumnName(c, e.getColumnOldName()); @@ -47,44 +63,103 @@ public void validate(Alter alter, AlterExpression e) { ColumnDropNotNull::getColumnName)); } - if (e.getColDataTypeList() != null) { + if (e.getColumnSetNotNullList() != null) { + validateOptionalColumnNames(c, ValidationUtil.map(e.getColumnSetNotNullList(), + ColumnSetNotNull::getColumnName)); + } + + if (e.hasActiveColumnDefinitions()) { boolean validateForExist = !EnumSet.of(AlterOperation.ADD).contains(e.getOperation()); validateOptionalColumnNames(c, ValidationUtil.map(e.getColDataTypeList(), ColumnDataType::getColumnName), validateForExist, NamedObject.table); + validateOptionalColumnNames(c, precedingColumnNames(e), NamedObject.table); } validateOptionalName(c, NamedObject.constraint, e.getConstraintName()); - if (e.getPkColumns() != null) { - validateOptionalColumnNames(c, e.getPkColumns()); + validateOptionalName(c, NamedObject.constraint, e.getNewConstraintName(), null, false, + NamedObject.table); + if (hasActiveDropColumns(e)) { + validateOptionalColumnNames(c, e.getDropColumns()); } - if (e.getFkColumns() != null) { - validateName(c, NamedObject.table, e.getFkSourceTable()); - validateOptionalColumnNames(c, e.getFkColumns()); - validateOptionalColumnNames(c, e.getFkSourceColumns()); - } + validateConstraint(c, definition); + validateIndex(c, e, definition); + } + } - if (e.getUk()) { - validateName(c, NamedObject.uniqueConstraint, e.getUkName()); - validateOptionalColumnNames(c, e.getUkColumns(), NamedObject.uniqueConstraint); + private void validateConstraint(ValidationCapability capability, TableElement definition) { + if (!(definition instanceof NamedConstraint)) { + return; + } + NamedConstraint constraint = (NamedConstraint) definition; + validateOptionalName(capability, NamedObject.constraint, constraint.getName(), null, false, + NamedObject.table); + if (constraint instanceof ForeignKeyConstraint) { + validateOptionalColumnNames(capability, + ((ForeignKeyConstraint) constraint).getReferencedColumnNames()); + } + if (constraint instanceof KeyConstraint && constraint.getKind() == ConstraintKind.UNIQUE) { + validateOptionalName(capability, NamedObject.uniqueConstraint, + ((KeyConstraint) constraint).getIndexName(), null, false, NamedObject.table); + } + if (constraint instanceof ConstraintUsingIndex) { + validateName(capability, NamedObject.index, + ((ConstraintUsingIndex) constraint).getExistingIndexName()); + } + if (constraint instanceof KeyColumnSource) { + KeyColumnSource keys = (KeyColumnSource) constraint; + if (keys.getColumns() != null) { + validateOptionalColumnNames(capability, keys.getColumns().stream() + .filter(key -> !key.isExpression()).map(KeyElement::getColumnName) + .collect(toList()), NamedObject.constraint); } + } + } - if (e.getIndex() != null) { - validateName(c, NamedObject.index, e.getIndex().getName()); - if (e.getIndex().getColumns() != null) { - validateOptionalColumnNames(c, - e.getIndex().getColumns().stream() - .filter(cp -> !cp.isExpression()) - .map(cp -> cp.getColumnName()) - .collect(toList()), - NamedObject.index); - } - } + private boolean hasActiveDropColumns(AlterExpression expression) { + if (expression.getOperation() == null || expression.getDropColumns() == null) { + return false; + } + switch (expression.getOperation()) { + case DROP: + return expression.getColumnName() == null && !expression.getDropColumns().isEmpty(); + case DROP_UNIQUE: + return true; + case DROP_FOREIGN_KEY: + return expression.getConstraintName() == null; + default: + return false; } } + private void validateIndex(ValidationCapability capability, AlterExpression expression, + TableElement definition) { + Index index = expression.getIndex(); + boolean rename = expression.getOperation() == AlterOperation.RENAME_INDEX + || expression.getOperation() == AlterOperation.RENAME_KEY; + if (index == null || definition != index && !rename) { + return; + } + if (rename && expression.getOldIndex() != null) { + validateName(capability, NamedObject.index, expression.getOldIndex().getName()); + } + boolean introducedName = expression.getOperation() == AlterOperation.ADD || rename; + validateOptionalName(capability, NamedObject.index, index.getName(), null, !introducedName); + if (definition == index && index.getColumns() != null) { + validateOptionalColumnNames(capability, index.getColumns().stream() + .filter(key -> !key.isExpression()).map(KeyElement::getColumnName) + .collect(toList()), NamedObject.index); + } + } + /** Returns the existing columns named by AFTER positions. */ + private static List precedingColumnNames(AlterExpression expression) { + return expression.getColDataTypeList().stream().map(ColumnDataType::getPosition) + .filter(position -> position != null + && position.getKind() == ColumnPosition.Kind.AFTER) + .map(ColumnPosition::getColumnName).collect(toList()); + } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/AnalyzeValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/AnalyzeValidator.java index ac5de0c192..f2f3a1b584 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/AnalyzeValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/AnalyzeValidator.java @@ -19,7 +19,8 @@ public class AnalyzeValidator extends AbstractValidator { public void validate(Analyze analyze) { for (ValidationCapability c : getCapabilities()) { validateFeature(c, Feature.analyze); - validateName(c, NamedObject.table, analyze.getTable().getFullyQualifiedName(), true); + analyze.getTables().forEach(table -> validateName(c, NamedObject.table, + table.getFullyQualifiedName(), true)); } } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/CommentValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/CommentValidator.java new file mode 100644 index 0000000000..dcffd3e4a1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/CommentValidator.java @@ -0,0 +1,115 @@ +/*- + * #%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.util.validation.validator; + +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.comment.CommentTarget; +import net.sf.jsqlparser.util.validation.ValidationCapability; + +public class CommentValidator extends AbstractValidator { + @Override + public void validate(Comment comment) { + for (ValidationCapability capability : getCapabilities()) { + validateFeature(capability, Feature.comment); + validateOptionalFeature(capability, comment.getTable(), Feature.commentOnTable); + validateOptionalFeature(capability, comment.getColumn(), Feature.commentOnColumn); + validateOptionalFeature(capability, comment.getView(), Feature.commentOnView); + if (comment.getTarget() != null) { + validateFeature(capability, targetFeature(comment.getTarget().getKind())); + } + } + } + + private static Feature targetFeature(CommentTarget.Kind kind) { + switch (kind) { + case INDEX: + return Feature.commentOnIndex; + case SCHEMA: + return Feature.commentOnSchema; + case SEQUENCE: + return Feature.commentOnSequence; + case DOMAIN: + return Feature.commentOnDomain; + case TYPE: + return Feature.commentOnType; + case MATERIALIZED_VIEW: + return Feature.commentOnMaterializedView; + case FUNCTION: + return Feature.commentOnFunction; + case CONSTRAINT: + return Feature.commentOnConstraint; + case TRIGGER: + return Feature.commentOnTrigger; + case POLICY: + return Feature.commentOnPolicy; + case RULE: + return Feature.commentOnRule; + case PROCEDURE: + return Feature.commentOnProcedure; + case ROUTINE: + return Feature.commentOnRoutine; + case AGGREGATE: + return Feature.commentOnAggregate; + case CAST: + return Feature.commentOnCast; + case COLLATION: + return Feature.commentOnCollation; + case FOREIGN_TABLE: + return Feature.commentOnForeignTable; + case STATISTICS: + return Feature.commentOnStatistics; + case DATABASE: + return Feature.commentOnDatabase; + case ROLE: + return Feature.commentOnRole; + case TABLESPACE: + return Feature.commentOnTablespace; + case ACCESS_METHOD: + return Feature.commentOnAccessMethod; + case EXTENSION: + return Feature.commentOnExtension; + case FOREIGN_DATA_WRAPPER: + return Feature.commentOnForeignDataWrapper; + case LANGUAGE: + return Feature.commentOnLanguage; + case SERVER: + return Feature.commentOnServer; + case EVENT_TRIGGER: + return Feature.commentOnEventTrigger; + case PUBLICATION: + return Feature.commentOnPublication; + case SUBSCRIPTION: + return Feature.commentOnSubscription; + case OPERATOR: + return Feature.commentOnOperator; + case OPERATOR_CLASS: + return Feature.commentOnOperatorClass; + case OPERATOR_FAMILY: + return Feature.commentOnOperatorFamily; + case CONVERSION: + return Feature.commentOnConversion; + case LARGE_OBJECT: + return Feature.commentOnLargeObject; + case TRANSFORM: + return Feature.commentOnTransform; + case TEXT_SEARCH_CONFIGURATION: + return Feature.commentOnTextSearchConfiguration; + case TEXT_SEARCH_DICTIONARY: + return Feature.commentOnTextSearchDictionary; + case TEXT_SEARCH_PARSER: + return Feature.commentOnTextSearchParser; + case TEXT_SEARCH_TEMPLATE: + return Feature.commentOnTextSearchTemplate; + default: + throw new IllegalArgumentException("Unknown COMMENT target: " + kind); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateIndexValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateIndexValidator.java index d613784975..5d247036d2 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateIndexValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateIndexValidator.java @@ -9,11 +9,13 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.create.table.KeyElement; import static java.util.stream.Collectors.toList; import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.util.TableDefinitionTraversal; import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.metadata.NamedObject; @@ -33,11 +35,14 @@ public void validate(CreateIndex createIndex) { validateOptionalColumnNames(c, index.getColumns().stream() .filter(cp -> !cp.isExpression()) - .map(Index.ColumnParams::getColumnName) + .map(KeyElement::getColumnName) .collect(toList()), NamedObject.table); } } + TableDefinitionTraversal.visit(createIndex, this::validateOptionalExpression, table -> { + // Table and index names are validated above for each capability. + }); } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateSequenceValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateSequenceValidator.java index f2b95d897a..b6a99a5d12 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateSequenceValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateSequenceValidator.java @@ -22,6 +22,9 @@ public class CreateSequenceValidator extends AbstractValidator { @Override public void validate(CreateSequence statement) { + if (statement.getSequence().getOwnership() != null) { + validateOptionalExpression(statement.getSequence().getOwnership().getColumn()); + } for (ValidationCapability c : getCapabilities()) { validateFeature(Feature.createSequence); validateName(c, NamedObject.sequence, statement.getSequence().getFullyQualifiedName(), diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateTableValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateTableValidator.java index e4b7a3c83b..32d5c24966 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateTableValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateTableValidator.java @@ -12,6 +12,9 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.ConstraintDeclaration; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.util.TableDefinitionTraversal; import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.metadata.NamedObject; @@ -39,10 +42,22 @@ public void validate(CreateTable createTable) { validateName(c, NamedObject.index, i.getName()); } } + for (ConstraintDeclaration declaration : createTable.getConstraints()) { + NamedConstraint constraint = declaration.getConstraint(); + String name = constraint != null ? constraint.getName() + : declaration.getColumnOption().getForeignKeyReference() != null + ? declaration.getColumnOption().getForeignKeyReference() + .getConstraintName() + : null; + validateOptionalName(c, NamedObject.constraint, name, null, false, + NamedObject.table); + } validateName(c, NamedObject.table, createTable.getTable().getFullyQualifiedName(), false); } + TableDefinitionTraversal.visit(createTable, this::validateOptionalExpression, + this::validateOptionalFromItem); if (createTable.getSelect() != null) { getValidator(StatementValidator.class).validate(createTable.getSelect()); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidator.java index 12c406f2cd..b7659f12aa 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidator.java @@ -25,6 +25,7 @@ public class CreateViewValidator extends AbstractValidator { @Override public void validate(CreateView createView) { + createView.validateOptions(); for (ValidationCapability c : getCapabilities()) { validateFeature(c, Feature.createView); validateFeature(c, createView.isOrReplace(), Feature.createOrReplaceView); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java index 19588e3b7b..d030b9f309 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.util.validation.ValidationCapability; /** @@ -39,6 +40,7 @@ public void validate(Delete delete) { delete.getTables().forEach(t -> t.accept(v, null)); } + validateOptionalForPortionClause(delete.getForPortionClause()); validateOptionalExpression(delete.getWhere()); validateOptionalOrderByElements(delete.getOrderByElements()); @@ -48,6 +50,15 @@ public void validate(Delete delete) { getValidator(LimitValidator.class).validate(delete.getLimit()); } + if (delete.getOption() != null) { + for (OptionHint optionHint : delete.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } + if (delete.getReturningClause() != null) { delete.getReturningClause().forEach(c -> c.accept(v, null)); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/DropValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/DropValidator.java index 04ea1cf8f9..42dc044a41 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/DropValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/DropValidator.java @@ -45,9 +45,12 @@ public void validate(Drop drop) { Feature.dropSequenceIfExists); } + if (drop.getTable() != null) { + validateName(NamedObject.table, drop.getTable().getFullyQualifiedName()); + } NamedObject named = NamedObject.forName(type); if (Arrays.asList(NamedObject.table, NamedObject.view).contains(named)) { - validateName(named, drop.getName().getFullyQualifiedName()); + drop.getNames().forEach(name -> validateName(named, name.getFullyQualifiedName())); } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExecuteValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExecuteValidator.java index 3c91b91285..968bd13b15 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExecuteValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExecuteValidator.java @@ -25,6 +25,8 @@ public class ExecuteValidator extends AbstractValidator { public void validate(Execute execute) { for (ValidationCapability c : getCapabilities()) { validateFeature(c, Feature.execute); + validateFeature(c, ExecType.IMPLICIT.equals(execute.getExecType()), + Feature.oracleBlock); validateFeature(c, ExecType.EXECUTE.equals(execute.getExecType()), Feature.executeExecute); validateFeature(c, ExecType.EXEC.equals(execute.getExecType()), Feature.executeExec); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index a18a21ab79..6b2d4927c6 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -9,6 +9,11 @@ */ package net.sf.jsqlparser.util.validation.validator; +import java.util.Set; +import java.util.Collections; +import net.sf.jsqlparser.statement.select.MatchRecognize; + +import java.util.Map; import net.sf.jsqlparser.expression.AllValue; import net.sf.jsqlparser.expression.AnalyticExpression; import net.sf.jsqlparser.expression.AnyComparisonExpression; @@ -19,6 +24,7 @@ import net.sf.jsqlparser.expression.CaseExpression; import net.sf.jsqlparser.expression.CastExpression; import net.sf.jsqlparser.expression.CollateExpression; +import net.sf.jsqlparser.expression.ColumnsExpression; import net.sf.jsqlparser.expression.ConnectByPriorOperator; import net.sf.jsqlparser.expression.ConnectByRootOperator; import net.sf.jsqlparser.expression.DateTimeLiteralExpression; @@ -39,12 +45,13 @@ import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.JsonTableFunction; -import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.KeyExpression; import net.sf.jsqlparser.expression.LambdaExpression; +import net.sf.jsqlparser.expression.ListComprehension; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.LowExpression; +import net.sf.jsqlparser.expression.MapExpression; import net.sf.jsqlparser.expression.MySQLGroupConcat; import net.sf.jsqlparser.expression.NextValExpression; import net.sf.jsqlparser.expression.NotExpression; @@ -61,6 +68,7 @@ import net.sf.jsqlparser.expression.SignedExpression; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.StructType; +import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.TimeKeyExpression; import net.sf.jsqlparser.expression.TimeValue; import net.sf.jsqlparser.expression.TimestampValue; @@ -74,6 +82,7 @@ import net.sf.jsqlparser.expression.WindowOffset; import net.sf.jsqlparser.expression.WindowRange; import net.sf.jsqlparser.expression.XMLSerializeExpr; +import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -82,6 +91,7 @@ import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor; import net.sf.jsqlparser.expression.operators.arithmetic.Concat; import net.sf.jsqlparser.expression.operators.arithmetic.Division; +import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation; import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision; import net.sf.jsqlparser.expression.operators.arithmetic.Modulo; import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication; @@ -104,6 +114,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -142,6 +153,8 @@ @SuppressWarnings({"PMD.CyclomaticComplexity"}) public class ExpressionValidator extends AbstractValidator implements ExpressionVisitor { + private Set rowPatternVariables = Collections.emptySet(); + @Override public Void visit(Addition addition, S context) { visitBinaryExpression(addition, " + "); @@ -188,6 +201,12 @@ public Void visit(IntegerDivision division, S context) { return null; } + @Override + public Void visit(Exponentiation exponentiation, S context) { + visitBinaryExpression(exponentiation, " ** "); + return null; + } + @Override public Void visit(DoubleValue doubleValue, S context) { // nothing to validate @@ -529,13 +548,30 @@ public Void visit(ParenthesedSelect selectBody, S context) { return null; } + void validateMatchRecognizeExpressions(MatchRecognize matchRecognize) { + Set previous = rowPatternVariables; + try { + rowPatternVariables = matchRecognize.getPatternVariableNames(); + matchRecognize + .forEachPatternExpression( + expression -> validateOptionalExpression(expression, this)); + } finally { + rowPatternVariables = previous; + } + } + @Override public Void visit(Column tableColumn, S context) { if (tableColumn .getOldOracleJoinSyntax() != SupportsOldOracleJoinSyntax.NO_ORACLE_JOIN) { validateFeature(Feature.oracleOldJoinSyntax); } - validateName(NamedObject.column, tableColumn.getFullyQualifiedName()); + // A pattern qualifier is not a schema/table name. Resolving its columns + // requires binding the input relation, which metadata validation does not do. + if (tableColumn.getTable() == null || !rowPatternVariables.contains( + MatchRecognize.normalizeVariableName(tableColumn.getTable().getName()))) { + validateName(NamedObject.column, tableColumn.getFullyQualifiedName()); + } return null; } @@ -812,6 +848,12 @@ public Void visit(JsonOperator jsonExpr, S context) { return null; } + @Override + public Void visit(Intersects intersects, S context) { + visitBinaryExpression(intersects, " # "); + return null; + } + @Override public Void visit(UserVariable var, S context) { // nothing to validate @@ -913,6 +955,10 @@ public void visit(JsonOperator jsonExpr) { visit(jsonExpr, null); } + public void visit(Intersects intersects) { + visit(intersects, null); + } + public void visit(UserVariable var) { visit(var, null); } @@ -996,6 +1042,9 @@ public Void visit(ArrayExpression array, S context) { if (array.getStopIndexExpression() != null) { array.getStopIndexExpression().accept(this, context); } + if (array.getStepExpression() != null) { + array.getStepExpression().accept(this, context); + } return null; } @@ -1007,6 +1056,15 @@ public Void visit(ArrayConstructor aThis, S context) { return null; } + @Override + public Void visit(MapExpression mapExpression, S context) { + for (Map.Entry entry : mapExpression.getEntries()) { + entry.getKey().accept(this, context); + entry.getValue().accept(this, context); + } + return null; + } + @Override public void validate(Expression expression) { expression.accept(this, null); @@ -1029,6 +1087,10 @@ public Void visit(TimezoneExpression a, S context) { @Override public Void visit(XMLSerializeExpr xml, S context) { + xml.validateOptions(); + for (Expression expression : xml.getExpressions()) { + expression.accept(this, context); + } return null; } @@ -1126,7 +1188,8 @@ public Void visit(GeometryDistance geometryDistance, S context) { @Override public Void visit(Select select, S context) { - return null; + return select.accept((net.sf.jsqlparser.statement.select.SelectVisitor) getValidator( + SelectValidator.class), context); } @Override @@ -1148,8 +1211,20 @@ public Void visit(TrimFunction trimFunction, S context) { @Override public Void visit(RangeExpression rangeExpression, S context) { - rangeExpression.getStartExpression().accept(this, context); - rangeExpression.getEndExpression().accept(this, context); + for (Expression bound : new Expression[] {rangeExpression.getStartExpression(), + rangeExpression.getEndExpression(), rangeExpression.getStepExpression()}) { + if (bound != null) { + bound.accept(this, context); + } + } + return null; + } + + @Override + public Void visit(TernaryExpression ternaryExpression, S context) { + ternaryExpression.getCondition().accept(this, context); + ternaryExpression.getThenExpression().accept(this, context); + ternaryExpression.getElseExpression().accept(this, context); return null; } @@ -1183,6 +1258,26 @@ public Void visit(LambdaExpression lambdaExpression, S context) { return null; } + @Override + public Void visit(ListComprehension listComprehension, S context) { + listComprehension.getExpression().accept(this, context); + listComprehension.getList().accept(this, context); + if (listComprehension.getCondition() != null) { + listComprehension.getCondition().accept(this, context); + } + return null; + } + + @Override + public Void visit(ColumnsExpression columnsExpression, S context) { + for (Expression expression : columnsExpression.getAllExpressions()) { + if (expression != null) { + expression.accept(this, context); + } + } + return null; + } + @Override public Void visit(HighExpression highExpression, S context) { highExpression.getExpression().accept(this, context); @@ -1241,6 +1336,10 @@ public void visit(ArrayConstructor aThis) { visit(aThis, null); } + public void visit(MapExpression mapExpression) { + visit(mapExpression, null); + } + public void visit(VariableAssignment a) { visit(a, null); @@ -1310,6 +1409,10 @@ public void visit(RangeExpression rangeExpression) { visit(rangeExpression, null); } + public void visit(TernaryExpression ternaryExpression) { + visit(ternaryExpression, null); + } + public void visit(TSQLLeftJoin tsqlLeftJoin) { visit(tsqlLeftJoin, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/GrantValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/GrantValidator.java index ceb7bd9daa..952163a67a 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/GrantValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/GrantValidator.java @@ -23,14 +23,18 @@ public class GrantValidator extends AbstractValidator { public void validate(Grant grant) { for (ValidationCapability c : getCapabilities()) { validateFeature(c, Feature.grant); - if (isNotEmpty(grant.getUsers())) { - grant.getUsers().forEach(u -> validateName(NamedObject.user, u)); - } - if (grant.getRole() != null) { - validateName(NamedObject.role, grant.getRole()); + // PUBLIC and CURRENT_USER are not names + if (isNotEmpty(grant.getClause().getGranteeItems())) { + grant.getClause().getGranteeItems().stream() + .filter(grantee -> grantee.getAccount() != null) + .forEach(grantee -> validateName(NamedObject.user, + grantee.getAccount().toString())); } + grant.getClause().getRoleItems() + .forEach(role -> validateName(NamedObject.role, role.toString())); - // can't validate grant.getObjectName() - don't know the kind of this object. + grant.getClause().visit(this::validateOptionalFromItem, + this::validateOptionalExpression); } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/GroupByValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/GroupByValidator.java index ee2d7155f8..0d5ed5fe96 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/GroupByValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/GroupByValidator.java @@ -31,6 +31,9 @@ public void validate(GroupByElement groupBy) { public Void visit(GroupByElement groupBy, S context) { for (ValidationCapability c : getCapabilities()) { validateFeature(c, Feature.selectGroupBy); + if (groupBy.hasGroupBySortDirections()) { + validateFeature(c, Feature.selectGroupByOrdering); + } if (isNotEmpty(groupBy.getGroupingSets())) { validateFeature(c, Feature.selectGroupByGroupingSets); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/InsertBulkValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/InsertBulkValidator.java new file mode 100644 index 0000000000..dcf6a9af64 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/InsertBulkValidator.java @@ -0,0 +1,31 @@ +/*- + * #%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.util.validation.validator; + +import java.util.stream.Collectors; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.insert.InsertBulk; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.util.validation.ValidationCapability; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; + +public class InsertBulkValidator extends AbstractValidator { + @Override + public void validate(InsertBulk statement) { + validateFeature(Feature.insertBulk); + validateOptionalFromItem(statement.getTable()); + for (ValidationCapability capability : getCapabilities()) { + validateOptionalColumnNames(capability, statement.getColumns().stream() + .map(ColumnDefinition::getColumnName).collect(Collectors.toList()), + NamedObject.table); + } + statement.visitExpressions(this::validateOptionalExpression); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/InsertValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/InsertValidator.java index 99f6ad3ea2..717e10bf0d 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/InsertValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/InsertValidator.java @@ -94,6 +94,10 @@ public void validate(Insert insert) { } } + if (insert.getConflictTarget() != null) { + insert.getConflictTarget().accept(getValidator(ExpressionValidator.class), null); + } + if (insert.getReturningClause() != null) { SelectValidator v = getValidator(SelectValidator.class); insert.getReturningClause().forEach(c -> c.accept(v, null)); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/LimitValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/LimitValidator.java index 7089d67e6d..8d001d88da 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/LimitValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/LimitValidator.java @@ -27,5 +27,8 @@ public void validate(Limit limit) { validateFeature(c, limit.isLimitAll(), Feature.limitAll); validateOptionalFeature(c, limit.getOffset(), Feature.limitOffset); } + + validateOptionalExpression(limit.getRowCount()); + validateOptionalExpression(limit.getOffset()); } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/MatchRecognizeValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/MatchRecognizeValidator.java new file mode 100644 index 0000000000..051ca5dba3 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/MatchRecognizeValidator.java @@ -0,0 +1,245 @@ +/*- + * #%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.util.validation.validator; + +import java.math.BigInteger; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.RowPatternFunction; +import net.sf.jsqlparser.expression.UserVariable; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.schema.MultiPartName; +import net.sf.jsqlparser.statement.select.MatchRecognize; +import net.sf.jsqlparser.statement.select.RowPattern; +import net.sf.jsqlparser.statement.select.RowPatternVisitorAdapter; +import net.sf.jsqlparser.util.validation.ValidationCapability; +import net.sf.jsqlparser.util.validation.ValidationException; + +/** + * Static dialect checks; schema binding, function eligibility and runtime checks remain with the + * DB. + */ +public class MatchRecognizeValidator extends AbstractValidator { + @Override + public void validate(MatchRecognize match) { + String dialect = context().getAsString(Feature.dialect); + boolean bigQuery = Dialect.BIGQUERY.name().equals(dialect); + boolean oracle = Dialect.ORACLE.name().equals(dialect); + boolean snowflake = Dialect.SNOWFLAKE.name().equals(dialect); + if (!bigQuery && !oracle && !snowflake) { + return; + } + validateDialectOptions(match, bigQuery, oracle); + validateRowOutput(match, oracle); + Set used = validateVariables(match, bigQuery, oracle); + validateTargets(match, used, bigQuery); + match.getPattern().accept(new PatternValidator(match, bigQuery, oracle), null); + match.getMeasures() + .forEach(measure -> checkFunctions(measure.getExpression(), bigQuery, false)); + match.getDefinitions() + .forEach(definition -> checkFunctions(definition.getExpression(), bigQuery, true)); + } + + private void validateDialectOptions(MatchRecognize match, boolean bigQuery, boolean oracle) { + if (bigQuery) { + if (match.getOrderByElements().isEmpty() || match.getMeasures().isEmpty()) { + error("BigQuery MATCH_RECOGNIZE requires ORDER BY and MEASURES"); + } + if (match.getRowsPerMatch() != null || match.getSkipVariable() != null) { + error("BigQuery does not support ROWS PER MATCH or SKIP TO a variable"); + } + } else if (match.getOptions() != null) { + error("MATCH_RECOGNIZE OPTIONS is specific to BigQuery"); + } + if (!oracle && !match.getSubsets().isEmpty()) { + error("SUBSET is specific to Oracle"); + } + } + + private void validateRowOutput(MatchRecognize match, boolean oracle) { + if (match.getEmptyMatchMode() != null + && match.getRowsPerMatch() != MatchRecognize.RowsPerMatch.ALL) { + error("Empty match options require ALL ROWS PER MATCH"); + } + match.getMeasures().forEach(measure -> { + if (measure.getAlias() == null || oracle && !measure.getAlias().isUseAs()) { + error("A measure requires an alias (with AS in Oracle)"); + } + }); + + } + + private Set validateVariables(MatchRecognize match, boolean bigQuery, boolean oracle) { + Set used = new HashSet<>(); + match.getPattern().accept(new RowPatternVisitorAdapter() { + @Override + public Void visit(RowPattern.Variable variable, S context) { + used.add(normalize(variable.getName(), bigQuery)); + return null; + } + }, null); + Set defined = new HashSet<>(); + for (MatchRecognize.Definition definition : match.getDefinitions()) { + String name = normalize(definition.getName(), bigQuery); + if (!defined.add(name)) { + error("Duplicate row pattern variable: " + definition.getName()); + } + if ((bigQuery || oracle) && !used.contains(name)) { + error("Unused row pattern variable: " + definition.getName()); + } + } + if (bigQuery) { + for (String name : used) { + if (!defined.contains(name)) { + error("Undefined row pattern variable: " + name); + } + } + } + return used; + } + + private void validateTargets(MatchRecognize match, Set used, boolean bigQuery) { + Set subsetNames = new HashSet<>(); + for (MatchRecognize.Subset subset : match.getSubsets()) { + String name = normalize(subset.getName(), false); + if (used.contains(name) || !subsetNames.add(name)) { + error("Duplicate SUBSET variable: " + subset.getName()); + } + for (String variable : subset.getVariables()) { + if (!used.contains(normalize(variable, false))) { + error("SUBSET requires a primary pattern variable: " + variable); + } + } + } + if (match.getSkipVariable() != null + && !used.contains(normalize(match.getSkipVariable(), bigQuery)) + && !subsetNames.contains(normalize(match.getSkipVariable(), false))) { + error("Unknown AFTER MATCH SKIP variable: " + match.getSkipVariable()); + } + } + + private class PatternValidator extends RowPatternVisitorAdapter { + private final MatchRecognize match; + private final boolean bigQuery; + private final boolean oracle; + + private PatternValidator(MatchRecognize match, boolean bigQuery, boolean oracle) { + this.match = match; + this.bigQuery = bigQuery; + this.oracle = oracle; + } + + @Override + public Void visit(RowPattern.Quantified quantified, S context) { + checkBound(quantified.getLowerBound(), bigQuery); + checkBound(quantified.getUpperBound(), bigQuery); + if (quantified.getLowerBound() instanceof LongValue + && quantified.getUpperBound() instanceof LongValue + && ((LongValue) quantified.getLowerBound()).getBigIntegerValue() + .compareTo(((LongValue) quantified.getUpperBound()) + .getBigIntegerValue()) > 0) { + error("Row pattern upper bound is smaller than its lower bound"); + } + if (bigQuery && quantified.getType() == RowPattern.Quantified.Type.EXACT + && quantified.isReluctant()) { + error("BigQuery does not support reluctant fixed quantifiers"); + } + if (oracle) { + Expression maximum = quantified.getType() == RowPattern.Quantified.Type.EXACT + ? quantified.getLowerBound() + : quantified.getUpperBound(); + if (maximum instanceof LongValue + && ((LongValue) maximum).getBigIntegerValue().signum() == 0) { + error("Oracle row pattern quantifier maximum must be positive"); + } + } + return super.visit(quantified, context); + } + + @Override + public Void visit(RowPattern.Operation operation, S context) { + if (oracle && operation.getType() == RowPattern.Operation.Type.ALTERNATION + && operation.getPatterns().stream() + .anyMatch(RowPattern.Empty.class::isInstance)) { + error("Oracle requires a pattern on each side of an alternative"); + } + return super.visit(operation, context); + } + + @Override + public Void visit(RowPattern.Exclusion exclusion, S context) { + if (bigQuery) { + error("BigQuery does not support row pattern exclusion"); + } + if (match.getEmptyMatchMode() == MatchRecognize.EmptyMatchMode.WITH_UNMATCHED) { + error("Row pattern exclusion cannot be combined with WITH UNMATCHED ROWS"); + } + return super.visit(exclusion, context); + } + + @Override + public Void visit(RowPattern.Permute permute, S context) { + if (bigQuery) { + error("BigQuery does not support PERMUTE"); + } + return super.visit(permute, context); + } + } + + private void checkFunctions(Expression expression, boolean bigQuery, boolean definition) { + expression.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(RowPatternFunction function, S context) { + if (bigQuery) { + error("BigQuery does not support RUNNING or FINAL function prefixes"); + } + if (definition && function + .getEvaluationMode() == RowPatternFunction.EvaluationMode.FINAL) { + error("FINAL is only allowed in MATCH_RECOGNIZE MEASURES"); + } + return function.getFunction().accept(this, context); + } + }, null); + } + + private static String normalize(String name, boolean bigQuery) { + String unquoted = MultiPartName.unquote(name); + return bigQuery || name.equals(unquoted) ? unquoted.toUpperCase(Locale.ROOT) : unquoted; + } + + private void checkBound(Expression bound, boolean bigQuery) { + if (bound == null) { + return; + } + if (bound instanceof LongValue) { + BigInteger value = ((LongValue) bound).getBigIntegerValue(); + if (value.signum() < 0 || bigQuery && value.compareTo(BigInteger.valueOf(10000)) > 0) { + error(bigQuery ? "BigQuery row pattern bounds must be between 0 and 10000" + : "Row pattern bounds must be nonnegative"); + } + } else if (!bigQuery || !(bound instanceof JdbcParameter) + && !(bound instanceof UserVariable && !((UserVariable) bound).isDoubleAdd())) { + error(bigQuery ? "A row pattern bound must be an integer literal or query parameter" + : "A row pattern bound must be an integer literal"); + } + } + + private void error(String message) { + for (ValidationCapability capability : getCapabilities()) { + putError(capability, new ValidationException(message)); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java index e82ebc91dc..ab5dfa9b1d 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java @@ -11,7 +11,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.merge.*; -import net.sf.jsqlparser.statement.update.UpdateSet; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.util.validation.ValidationCapability; /** @@ -26,11 +26,23 @@ public void validate(Merge merge) { for (ValidationCapability c : getCapabilities()) { validateFeature(c, Feature.merge); } + if (merge.getReturningClause() != null) { + SelectValidator validator = getValidator(SelectValidator.class); + merge.getReturningClause().forEach(item -> item.accept(validator, null)); + } validateOptionalExpression(merge.getOnCondition()); if (merge.getOperations() != null) { merge.getOperations().forEach(operation -> operation.accept(this, null)); } validateOptionalFromItems(merge.getFromItem()); + if (merge.getOption() != null) { + for (OptionHint optionHint : merge.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } } @Override @@ -46,10 +58,7 @@ public void visit(MergeDelete mergeDelete) { @Override public Void visit(MergeUpdate mergeUpdate, S context) { validateOptionalExpression(mergeUpdate.getAndPredicate()); - for (UpdateSet updateSet : mergeUpdate.getUpdateSets()) { - validateOptionalExpressions(updateSet.getColumns()); - validateOptionalExpressions(updateSet.getValues()); - } + validateOptionalUpdateSets(mergeUpdate.getUpdateSets()); validateOptionalExpression(mergeUpdate.getDeleteWhereCondition()); validateOptionalExpression(mergeUpdate.getWhereCondition()); return null; diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java index 858702ddae..8e04d7df95 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java @@ -32,6 +32,12 @@ public Void visit(OrderByElement orderBy, S context) { validateOptionalFeature(c, orderBy.getNullOrdering(), Feature.orderByNullOrdering); } getValidator(ExpressionValidator.class).validate(orderBy.getExpression()); + if (orderBy.getWithFill() != null) { + validateOptionalExpression(orderBy.getWithFill().getFrom()); + validateOptionalExpression(orderBy.getWithFill().getTo()); + validateOptionalExpression(orderBy.getWithFill().getStep()); + validateOptionalExpression(orderBy.getWithFill().getStaleness()); + } return null; } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java index 5cab04030e..b06c8a89f5 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java @@ -9,10 +9,13 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.select.MatchRecognize; + import java.util.List; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.MySQLIndexHint; import net.sf.jsqlparser.expression.SQLServerHints; +import net.sf.jsqlparser.expression.WindowDefinition; import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.imprt.Import; @@ -21,15 +24,21 @@ import net.sf.jsqlparser.statement.select.Fetch; import net.sf.jsqlparser.statement.select.ForMode; import net.sf.jsqlparser.statement.select.FromItemVisitor; +import net.sf.jsqlparser.statement.select.InterpolateElement; import net.sf.jsqlparser.statement.select.IntersectOp; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; import net.sf.jsqlparser.statement.select.MinusOp; import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause; +import net.sf.jsqlparser.statement.select.MySqlSelectModifier; import net.sf.jsqlparser.statement.select.Offset; +import net.sf.jsqlparser.statement.select.OptionClause; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Pivot; +import net.sf.jsqlparser.statement.select.PivotQuery; +import net.sf.jsqlparser.statement.select.UnPivotQuery; import net.sf.jsqlparser.statement.select.PivotVisitor; import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -55,6 +64,22 @@ public class SelectValidator extends AbstractValidator> PivotVisitor { @SuppressWarnings({"PMD.CyclomaticComplexity"}) + @Override + public Void visit(MatchRecognize matchRecognize, S context) { + validateFeature(Feature.matchRecognize); + if (matchRecognize.getOptions() != null) { + validateFeature(Feature.matchRecognizeOptions); + } + validateOptionalFromItem(matchRecognize.getInput()); + validateOptional(matchRecognize.getPivot(), p -> p.accept(this, context)); + validateOptional(matchRecognize.getUnPivot(), p -> p.accept(this, context)); + validateOptionalExpressions(matchRecognize.getPartitionBy()); + validateOptionalOrderByElements(matchRecognize.getOrderByElements()); + getValidator(MatchRecognizeValidator.class).validate(matchRecognize); + getValidator(ExpressionValidator.class).validateMatchRecognizeExpressions(matchRecognize); + return null; + } + @Override public Void visit(PlainSelect plainSelect, S context) { if (isNotEmpty(plainSelect.getWithItemsList())) { @@ -84,7 +109,16 @@ public Void visit(PlainSelect plainSelect, S context) { validateFeature(c, plainSelect.getMySqlSqlCacheFlag() != null, Feature.mysqlSqlCacheFlag); validateFeature(c, plainSelect.getMySqlSqlCalcFoundRows(), Feature.mysqlCalcFoundRows); + validateFeature(c, plainSelect.getMySqlSelectModifiers().stream() + .anyMatch(modifier -> modifier == MySqlSelectModifier.HIGH_PRIORITY + || modifier == MySqlSelectModifier.SQL_SMALL_RESULT + || modifier == MySqlSelectModifier.SQL_BIG_RESULT + || modifier == MySqlSelectModifier.SQL_BUFFER_RESULT), + Feature.mysqlSelectModifiers); validateOptionalFeature(c, plainSelect.getIntoTables(), Feature.selectInto); + validateOptionalFeature(c, plainSelect.getIntoTempTable(), Feature.selectInto); + validateOptionalFeature(c, plainSelect.getMySqlSelectIntoClause(), + Feature.selectInto); validateOptionalFeature(c, plainSelect.getKsqlWindow(), Feature.kSqlWindow); validateFeature(c, isNotEmpty(plainSelect.getOrderByElements()) && plainSelect.isOracleSiblings(), @@ -119,10 +153,17 @@ public Void visit(PlainSelect plainSelect, S context) { validateOptionalFromItems(plainSelect.getIntoTables()); validateOptionalJoins(plainSelect.getJoins()); - // to correctly recognize aliased tables - // @todo: fix this properly, I don't understand functional syntax - // validateOptionalList(plainSelect.getSelectItems(), () -> this, SelectItem::accept, - // context); + // after the FROM items, to correctly recognize aliased tables + if (plainSelect.getDistinct() != null + && isNotEmpty(plainSelect.getDistinct().getOnSelectItems())) { + plainSelect.getDistinct().getOnSelectItems() + .forEach(item -> item.accept(this, context)); + } + validateOptional(plainSelect.getTop(), + top -> validateOptionalExpression(top.getExpression())); + if (isNotEmpty(plainSelect.getSelectItems())) { + plainSelect.getSelectItems().forEach(item -> item.accept(this, context)); + } validateOptionalExpression(plainSelect.getPreWhere()); validateOptionalExpression(plainSelect.getWhere()); @@ -135,7 +176,16 @@ public Void visit(PlainSelect plainSelect, S context) { } validateOptionalExpression(plainSelect.getHaving()); + validateOptionalExpression(plainSelect.getQualify()); + if (isNotEmpty(plainSelect.getWindowDefinitions())) { + for (WindowDefinition windowDefinition : plainSelect.getWindowDefinitions()) { + validateOptionalExpressions(windowDefinition.getPartitionExpressionList()); + validateOptionalOrderByElements(windowDefinition.getOrderByElements()); + } + } validateOptionalOrderByElements(plainSelect.getOrderByElements()); + validateOptionalInterpolate(plainSelect.getInterpolate()); + validateOptionalOption(plainSelect.getOption()); if (plainSelect.getLimit() != null) { getValidator(LimitValidator.class).validate(plainSelect.getLimit()); @@ -154,6 +204,55 @@ public Void visit(PlainSelect plainSelect, S context) { return null; } + @Override + public Void visit(UnPivotQuery unPivotQuery, S context) { + validateFeature(Feature.pivot); + validateOptionalFromItem(unPivotQuery.getFromItem()); + validateOptionalExpressions(unPivotQuery.getOnExpressions()); + return null; + } + + @Override + public Void visit(PivotQuery pivotQuery, S context) { + if (isNotEmpty(pivotQuery.getWithItemsList())) { + pivotQuery.getWithItemsList() + .forEach(withItem -> withItem.accept((SelectVisitor) this, context)); + } + + validateFeature(Feature.pivot); + validateOptionalFromItem(pivotQuery.getFromItem()); + validateOptionalExpressions(pivotQuery.getOnExpressions()); + if (isNotEmpty(pivotQuery.getUsingItems())) { + pivotQuery.getUsingItems().forEach(item -> item.accept(this, context)); + } + validateOptionalExpressions(pivotQuery.getGroupByExpressions()); + validateOptionalOrderByElements(pivotQuery.getOrderByElements()); + validateOptionalInterpolate(pivotQuery.getInterpolate()); + validateOptionalOption(pivotQuery.getOption()); + + if (pivotQuery.getLimit() != null) { + getValidator(LimitValidator.class).validate(pivotQuery.getLimit()); + } + if (pivotQuery.getOffset() != null) { + validateOffset(pivotQuery.getOffset()); + } + if (pivotQuery.getFetch() != null) { + validateFetch(pivotQuery.getFetch()); + } + return null; + } + + private void validateOptionalOption(OptionClause option) { + if (option != null) { + for (OptionHint optionHint : option.getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } + } + private void validateMySqlSelectIntoClause(MySqlSelectIntoClause mySqlSelectIntoClause) { validateOptionalExpression(mySqlSelectIntoClause.getFileName()); validateOptionalExpression(mySqlSelectIntoClause.getFieldsTerminatedBy()); @@ -163,6 +262,15 @@ private void validateMySqlSelectIntoClause(MySqlSelectIntoClause mySqlSelectInto validateOptionalExpression(mySqlSelectIntoClause.getLinesTerminatedBy()); } + private void validateOptionalInterpolate(List interpolate) { + if (interpolate != null) { + for (InterpolateElement element : interpolate) { + validateOptionalExpression(element.getColumn()); + validateOptionalExpression(element.getExpression()); + } + } + } + @Override public Void visit(SelectItem selectExpressionItem, S context) { selectExpressionItem.getExpression().accept(getValidator(ExpressionValidator.class), @@ -183,8 +291,12 @@ public Void visit(ParenthesedSelect selectBody, S context) { @Override public Void visit(Table table, S context) { - validateNameWithAlias(NamedObject.table, table.getFullyQualifiedName(), - ValidationUtil.getAlias(table.getAlias())); + if (table.isTableVariable()) { + validateFeature(Feature.tableVariable); + } else { + validateNameWithAlias(NamedObject.table, table.getFullyQualifiedName(), + ValidationUtil.getAlias(table.getAlias())); + } validateOptional(table.getPivot(), p -> p.accept(this, context)); validateOptional(table.getUnPivot(), up -> up.accept(this, context)); @@ -235,6 +347,8 @@ public void validateOffset(Offset offset) { validateFeature(c, Feature.offset); validateOptionalFeature(c, offset.getOffsetParam(), Feature.offsetParam); } + + validateOptionalExpression(offset.getOffset()); } public void validateFetch(Fetch fetch) { @@ -244,7 +358,7 @@ public void validateFetch(Fetch fetch) { validateFeature(c, !fetch.isFetchParamFirst(), Feature.fetchNext); } - validateOptionalExpression(fetch.getFetchJdbcParameter()); + validateOptionalExpression(fetch.getExpression()); } public void validateOptionalJoins(List joins) { @@ -268,8 +382,10 @@ public void validateOptionalJoin(Join join) { validateFeature(c, join.isOuter(), Feature.joinOuter); validateFeature(c, join.isInner(), Feature.joinInner); validateFeature(c, join.isSemi(), Feature.joinSemi); + validateFeature(c, join.isAnti(), Feature.joinAnti); validateFeature(c, join.isStraight(), Feature.joinStraight); validateFeature(c, join.isApply(), Feature.joinApply); + validateFeature(c, join.isAsOf(), Feature.joinAsOf); validateFeature(c, join.isWindowJoin(), Feature.joinWindow); validateOptionalFeature(c, join.getUsingColumns(), Feature.joinUsingColumns); } @@ -308,6 +424,8 @@ public Void visit(SetOperationList setOperation, S context) { } validateOptionalOrderByElements(setOperation.getOrderByElements()); + validateOptionalInterpolate(setOperation.getInterpolate()); + validateOptionalOption(setOperation.getOption()); if (setOperation.getLimit() != null) { getValidator(LimitValidator.class).validate(setOperation.getLimit()); @@ -332,7 +450,13 @@ public Void visit(WithItem withItem, S context) { if (isNotEmpty(withItem.getWithItemList())) { withItem.getWithItemList().forEach(wi -> wi.accept(this, context)); } - withItem.getSelect().accept((SelectVisitor) this, context); + if (withItem.getCycleClause() != null) { + withItem.getCycleClause().accept(getValidator(ExpressionValidator.class), context); + } + if (withItem.getExpression() != null) { + withItem.getExpression().accept(getValidator(ExpressionValidator.class), context); + } + withItem.accept(getValidator(StatementValidator.class), context); return null; } @@ -360,6 +484,7 @@ public Void visit(TableStatement tableStatement, S context) { @Override public Void visit(TableFunction tableFunction, S context) { validateFeature(Feature.tableFunction); + validateOptionalExpressions(tableFunction.getFunctions()); validateOptional(tableFunction.getPivot(), p -> p.accept(this, context)); validateOptional(tableFunction.getUnPivot(), up -> up.accept(this, context)); @@ -393,6 +518,15 @@ public void visit(PlainSelect plainSelect) { visit(plainSelect, null); } + public void visit(PivotQuery pivotQuery) { + visit(pivotQuery, null); + } + + @Override + public void visit(UnPivotQuery unPivotQuery) { + visit(unPivotQuery, null); + } + public void visit(SelectItem selectExpressionItem) { visit(selectExpressionItem, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SetIdentityInsertValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SetIdentityInsertValidator.java new file mode 100644 index 0000000000..9bb0b49537 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SetIdentityInsertValidator.java @@ -0,0 +1,22 @@ +/*- + * #%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.util.validation.validator; + +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.SetIdentityInsertStatement; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; + +public class SetIdentityInsertValidator extends AbstractValidator { + @Override + public void validate(SetIdentityInsertStatement statement) { + validateFeatureAndName(Feature.setIdentityInsert, NamedObject.table, + statement.getTable().getFullyQualifiedName()); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SetStatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SetStatementValidator.java index 9e66b47be0..d62ced8a19 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/SetStatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SetStatementValidator.java @@ -23,6 +23,14 @@ public class SetStatementValidator extends AbstractValidator { public void validate(SetStatement set) { for (ValidationCapability c : getCapabilities()) { validateFeature(c, Feature.set); + if (set.getOnOffOptions() != null) { + validateFeature(c, Feature.sqlServerSetOptions); + } + for (int i = 0; i < set.getCount(); i++) { + if (set.getAssignmentOperator(i) == SetStatement.AssignmentOperator.TO) { + validateFeature(c, Feature.setAssignmentTo); + } + } } for (int i = 0; i < set.getCount(); i++) { validateOptionalExpressions(set.getExpressions(i)); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidator.java index e917d9e8ce..4cb91dbf45 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidator.java @@ -20,6 +20,11 @@ public class ShowColumnsStatementValidator extends AbstractValidator { + + @Override + public void validate(ShowCreateStatement show) { + for (ValidationCapability c : getCapabilities()) { + validateFeature(c, Feature.showCreate); + NamedObject namedObject = namedObject(show.getObjectType()); + if (namedObject == NamedObject.database) { + validateOptionalName(c, namedObject, show.getDatabaseName()); + } else if (namedObject != null && show.getName() != null) { + validateName(c, namedObject, show.getName().getFullyQualifiedName()); + } + } + } + + /** The metadata kind checked for the shown object, or null if there is none. */ + private static NamedObject namedObject(ShowCreateStatement.ObjectType type) { + if (type == null) { + return null; + } + switch (type) { + case TABLE: + return NamedObject.table; + case VIEW: + return NamedObject.view; + case PROCEDURE: + return NamedObject.procedure; + case DATABASE: + return NamedObject.database; + default: + // functions, triggers, events and accounts are not looked up + return null; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidator.java new file mode 100644 index 0000000000..1903f358b9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidator.java @@ -0,0 +1,27 @@ +/*- + * #%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.util.validation.validator; + +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; +import net.sf.jsqlparser.util.validation.ValidationCapability; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; + +public class ShowTableStatusStatementValidator + extends AbstractValidator { + + @Override + public void validate(ShowTableStatusStatement show) { + for (ValidationCapability c : getCapabilities()) { + validateFeature(c, Feature.showTableStatus); + validateOptionalName(c, NamedObject.database, show.getDbName()); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index 9e073a227c..e15bded789 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -9,10 +9,60 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespace; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; + +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; +import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterRoutine; +import net.sf.jsqlparser.statement.alter.AlterTrigger; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.ReassignOwned; +import net.sf.jsqlparser.statement.role.RoleOption; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; +import net.sf.jsqlparser.schema.Table; + +import net.sf.jsqlparser.statement.create.type.CreateType; +import net.sf.jsqlparser.statement.alter.AlterType; +import net.sf.jsqlparser.statement.create.domain.CreateDomain; +import net.sf.jsqlparser.statement.alter.AlterDomain; +import net.sf.jsqlparser.statement.create.extension.CreateExtension; +import net.sf.jsqlparser.statement.alter.AlterExtension; +import net.sf.jsqlparser.statement.create.publication.CreatePublication; +import net.sf.jsqlparser.statement.alter.AlterPublication; +import net.sf.jsqlparser.statement.create.subscription.CreateSubscription; +import net.sf.jsqlparser.statement.alter.AlterSubscription; + import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.Block; import net.sf.jsqlparser.statement.Commit; +import net.sf.jsqlparser.statement.StartTransaction; +import net.sf.jsqlparser.statement.ReleaseSavepointStatement; import net.sf.jsqlparser.statement.CreateFunctionalStatement; +import net.sf.jsqlparser.statement.DoStatement; import net.sf.jsqlparser.statement.DeclareStatement; import net.sf.jsqlparser.statement.DescribeStatement; import net.sf.jsqlparser.statement.ExplainStatement; @@ -23,6 +73,9 @@ import net.sf.jsqlparser.statement.SavepointStatement; import net.sf.jsqlparser.statement.SessionStatement; import net.sf.jsqlparser.statement.SetStatement; +import net.sf.jsqlparser.statement.SetDefaultRoleStatement; +import net.sf.jsqlparser.statement.SetPasswordStatement; +import net.sf.jsqlparser.statement.SetRoleStatement; import net.sf.jsqlparser.statement.ShowColumnsStatement; import net.sf.jsqlparser.statement.ShowStatement; import net.sf.jsqlparser.statement.Statement; @@ -30,13 +83,22 @@ import net.sf.jsqlparser.statement.Statements; import net.sf.jsqlparser.statement.UnsupportedStatement; import net.sf.jsqlparser.statement.UseStatement; +import net.sf.jsqlparser.statement.SetIdentityInsertStatement; +import net.sf.jsqlparser.statement.TableMaintenanceStatement; import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.statement.alter.AlterSession; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; +import net.sf.jsqlparser.statement.alter.AlterUser; +import net.sf.jsqlparser.statement.alter.RenameUser; 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.alter.database.AlterDatabase; +import net.sf.jsqlparser.statement.create.event.AlterEvent; +import net.sf.jsqlparser.statement.create.event.CreateEvent; import net.sf.jsqlparser.statement.create.function.CreateFunction; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; @@ -45,6 +107,10 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -55,20 +121,38 @@ import net.sf.jsqlparser.statement.grant.Grant; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.insert.InsertBulk; import net.sf.jsqlparser.statement.insert.ParenthesedInsert; import net.sf.jsqlparser.statement.lock.LockStatement; import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.upsert.Upsert; -import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.metadata.NamedObject; +import net.sf.jsqlparser.statement.PragmaStatement; +import net.sf.jsqlparser.statement.ExtensionStatement; +import net.sf.jsqlparser.statement.AttachStatement; +import net.sf.jsqlparser.statement.DetachStatement; +import net.sf.jsqlparser.statement.ConnectStatement; +import net.sf.jsqlparser.statement.DisconnectStatement; +import net.sf.jsqlparser.statement.PrepareStatement; +import net.sf.jsqlparser.statement.DeallocateStatement; +import net.sf.jsqlparser.statement.CopyStatement; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.AssertStatement; +import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; /** * @author gitmotte @@ -87,6 +171,65 @@ public Void visit(CreateTable createTable, S context) { return null; } + @Override + public Void visit(CreateTrigger createTrigger, S context) { + if (createTrigger.getRoutine() != null) { + validateFeature(Feature.createTrigger); + } + createTrigger.visit(this::validateOptionalFromItem, + this::validateOptionalExpression); + if (createTrigger.getBody() != null) { + createTrigger.getBody().accept(this, context); + } + return null; + } + + @Override + public Void visit(CreateEventTrigger statement, S context) { + // the filter values are tags and the function is declared, not called + validateFeature(Feature.createEventTrigger); + return null; + } + + @Override + public Void visit(AlterEventTrigger statement, S context) { + validateFeature(Feature.alterEventTrigger); + return null; + } + + @Override + public Void visit(Reindex statement, S context) { + validateFeature(Feature.reindex); + if (statement.isConcurrently() || statement.getOptions().stream() + .anyMatch(option -> option.getName() == Reindex.Option.Name.CONCURRENTLY)) { + validateFeature(Feature.reindexConcurrently); + } + if (statement.getName() != null) { + validateName(reindexedObject(statement.getTarget()), + statement.getName().getFullyQualifiedName()); + } + return null; + } + + private static NamedObject reindexedObject(Reindex.Target target) { + switch (target) { + case INDEX: + return NamedObject.index; + case TABLE: + return NamedObject.table; + case SCHEMA: + return NamedObject.schema; + default: + return NamedObject.database; + } + } + + @Override + public Void visit(SecurityLabel statement, S context) { + validateFeature(Feature.securityLabel); + return null; + } + @Override public Void visit(CreateView createView, S context) { getValidator(CreateViewValidator.class).validate(createView); @@ -128,6 +271,12 @@ public Void visit(Drop drop, S context) { return null; } + @Override + public Void visit(ReassignOwned statement, S context) { + validateFeature(Feature.reassignOwned); + return null; + } + @Override public Void visit(Insert insert, S context) { getValidator(InsertValidator.class).validate(insert); @@ -151,7 +300,7 @@ public Void visit(Select select, S context) { @Override public Void visit(Truncate truncate, S context) { validateFeature(Feature.truncate); - validateOptionalFromItem(truncate.getTable()); + truncate.getTables().forEach(this::validateOptionalFromItem); return null; } @@ -172,6 +321,14 @@ public Void visit(Alter alter, S context) { return null; } + @Override + public Void visit(AlterEvent alterEvent, S context) { + if (alterEvent.getBody() != null) { + alterEvent.getBody().accept(this, context); + } + return null; + } + @Override public Void visit(Statements statements, S context) { statements.forEach(s -> s.accept(this, context)); @@ -184,6 +341,18 @@ public Void visit(Execute execute, S context) { return null; } + @Override + public Void visit(DoStatement statement, S context) { + validateFeature(Feature.doStatement); + return null; + } + + @Override + public Void visit(InsertBulk statement, S context) { + getValidator(InsertBulkValidator.class).validate(statement); + return null; + } + @Override public Void visit(SetStatement set, S context) { getValidator(SetStatementValidator.class).validate(set); @@ -202,9 +371,43 @@ public Void visit(Merge merge, S context) { return null; } + @Override + public Void visit(StartTransaction statement, S context) { + validateFeature(Feature.startTransaction); + for (StartTransaction.Mode mode : statement.getModes()) { + switch (mode) { + case ISOLATION_LEVEL_SERIALIZABLE: + case ISOLATION_LEVEL_REPEATABLE_READ: + case ISOLATION_LEVEL_READ_COMMITTED: + case ISOLATION_LEVEL_READ_UNCOMMITTED: + validateFeature(Feature.transactionIsolationLevel); + break; + case DEFERRABLE: + case NOT_DEFERRABLE: + validateFeature(Feature.transactionDeferrable); + break; + case WITH_CONSISTENT_SNAPSHOT: + validateFeature(Feature.transactionConsistentSnapshot); + break; + default: + break; + } + } + return null; + } + + @Override + public Void visit(ReleaseSavepointStatement statement, S context) { + validateFeature(Feature.releaseSavepoint); + return null; + } + @Override public Void visit(Commit commit, S context) { validateFeature(Feature.commit); + if (commit.getChain() != null) { + validateFeature(Feature.transactionChain); + } return null; } @@ -214,6 +417,12 @@ public Void visit(Upsert upsert, S context) { return null; } + @Override + public Void visit(SetIdentityInsertStatement statement, S context) { + getValidator(SetIdentityInsertValidator.class).validate(statement); + return null; + } + @Override public Void visit(UseStatement use, S context) { getValidator(UseStatementValidator.class).validate(use); @@ -244,6 +453,18 @@ public Void visit(ShowTablesStatement showTables, S context) { return null; } + @Override + public Void visit(ShowTableStatusStatement showTableStatus, S context) { + getValidator(ShowTableStatusStatementValidator.class).validate(showTableStatus); + return null; + } + + @Override + public Void visit(ShowCreateStatement showCreate, S context) { + getValidator(ShowCreateStatementValidator.class).validate(showCreate); + return null; + } + @Override public Void visit(Block block, S context) { validateFeature(Feature.block); @@ -253,12 +474,7 @@ public Void visit(Block block, S context) { @Override public Void visit(Comment comment, S context) { - for (ValidationCapability c : getCapabilities()) { - validateFeature(c, Feature.comment); - validateOptionalFeature(c, comment.getTable(), Feature.commentOnTable); - validateOptionalFeature(c, comment.getColumn(), Feature.commentOnColumn); - validateOptionalFeature(c, comment.getView(), Feature.commentOnView); - } + getValidator(CommentValidator.class).validate(comment); return null; } @@ -299,6 +515,67 @@ public Void visit(CreateSchema aThis, S context) { return null; } + @Override + public Void visit(CreateDatabase aThis, S context) { + validateFeatureAndName(Feature.createDatabase, NamedObject.database, + aThis.getDatabaseName()); + return null; + } + + @Override + public Void visit(AlterDatabase statement, S context) { + validateFeature(Feature.alterDatabase); + if (statement.getParameterChange() != null + && statement.getParameterChange().getValues() != null) { + statement.getParameterChange().getValues().forEach(this::validateOptionalExpression); + } + return null; + } + + @Override + public Void visit(CreateUser createUser, S context) { + return null; + } + + @Override + public Void visit(AlterUser alterUser, S context) { + validateFeature(Feature.alterUser); + return null; + } + + @Override + public Void visit(RenameUser renameUser, S context) { + validateFeature(Feature.renameUser); + return null; + } + + @Override + public Void visit(SetRoleStatement setRole, S context) { + validateFeature(Feature.setRole); + return null; + } + + @Override + public Void visit(SetDefaultRoleStatement setDefaultRole, S context) { + validateFeature(Feature.setDefaultRole); + return null; + } + + @Override + public Void visit(SetPasswordStatement setPassword, S context) { + validateFeature(Feature.setPassword); + validateOptionalExpression(setPassword.getPassword()); + return null; + } + + @Override + public Void visit(CreateEvent createEvent, S context) { + if (createEvent.getBody() != null) { + createEvent.getBody().accept(this, context); + } + return null; + } + @Override public Void visit(CreateSequence createSequence, S context) { getValidator(CreateSequenceValidator.class).validate(createSequence); @@ -314,10 +591,27 @@ public Void visit(AlterSequence alterSequence, S context) { @Override public Void visit(CreateFunctionalStatement createFunctionalStatement, S context) { validateFeature(Feature.functionalStatement); + if (createFunctionalStatement + .getOperation() == CreateFunctionalStatement.Operation.CREATE_OR_ALTER) { + validateFeature(Feature.createOrAlterRoutine); + } + if (createFunctionalStatement.getReturnType() != null + && createFunctionalStatement.getReturnType().getTableElements() != null) { + createFunctionalStatement.getReturnType().getTableElements() + .forEach(element -> net.sf.jsqlparser.util.TableDefinitionTraversal.visit( + element, + this::validateOptionalExpression, this::validateOptionalFromItem)); + } if (createFunctionalStatement instanceof CreateFunction) { - validateFeature(Feature.createFunction); + validateFeature(createFunctionalStatement + .getOperation() == CreateFunctionalStatement.Operation.ALTER + ? Feature.alterFunction + : Feature.createFunction); } else if (createFunctionalStatement instanceof CreateProcedure) { - validateFeature(Feature.createProcedure); + validateFeature(createFunctionalStatement + .getOperation() == CreateFunctionalStatement.Operation.ALTER + ? Feature.alterProcedure + : Feature.createProcedure); } return null; } @@ -339,6 +633,12 @@ public Void visit(Analyze analyze, S context) { return null; } + @Override + public Void visit(TableMaintenanceStatement statement, S context) { + getValidator(TableMaintenanceValidator.class).validate(statement); + return null; + } + @Override public Void visit(SavepointStatement savepointStatement, S context) { // TODO: not yet implemented @@ -347,7 +647,9 @@ public Void visit(SavepointStatement savepointStatement, S context) { @Override public Void visit(RollbackStatement rollbackStatement, S context) { - // TODO: not yet implemented + if (rollbackStatement.getChain() != null) { + validateFeature(Feature.transactionChain); + } return null; } @@ -371,6 +673,90 @@ public Void visit(RenameTableStatement renameTableStatement, S context) { return null; } + @Override + public Void visit(PragmaStatement pragmaStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(AssertStatement assertStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(ExtensionStatement extensionStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(ExportDataStatement exportDataStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(AttachStatement attachStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(DetachStatement detachStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(ConnectStatement connectStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(DisconnectStatement disconnectStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(PrepareStatement prepareStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(DeallocateStatement deallocateStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(CopyStatement copyStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(CreateMacro createMacro, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(CreateExtensionRepository createExtensionRepository, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(LoadDataStatement loadDataStatement, S context) { + // TODO: not yet implemented + return null; + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { // TODO: not yet implemented @@ -415,6 +801,10 @@ public void visit(CreateTable createTable) { visit(createTable, null); } + public void visit(CreateTrigger createTrigger) { + visit(createTrigger, null); + } + public void visit(CreateView createView) { visit(createView, null); } @@ -435,6 +825,10 @@ public void visit(Drop drop) { visit(drop, null); } + public void visit(ReassignOwned statement) { + visit(statement, null); + } + public void visit(Insert insert) { visit(insert, null); } @@ -455,6 +849,10 @@ public void visit(Alter alter) { visit(alter, null); } + public void visit(AlterEvent alterEvent) { + visit(alterEvent, null); + } + public void visit(Statements statements) { visit(statements, null); } @@ -503,6 +901,16 @@ public void visit(ShowTablesStatement showTables) { visit(showTables, null); } + @Override + public void visit(ShowTableStatusStatement showTableStatus) { + visit(showTableStatus, null); + } + + @Override + public void visit(ShowCreateStatement showCreate) { + visit(showCreate, null); + } + public void visit(Block block) { visit(block, null); } @@ -531,6 +939,34 @@ public void visit(CreateSchema aThis) { visit(aThis, null); } + public void visit(CreateUser createUser) { + visit(createUser, null); + } + + public void visit(AlterUser alterUser) { + visit(alterUser, null); + } + + public void visit(RenameUser renameUser) { + visit(renameUser, null); + } + + public void visit(SetRoleStatement setRole) { + visit(setRole, null); + } + + public void visit(SetDefaultRoleStatement setDefaultRole) { + visit(setDefaultRole, null); + } + + public void visit(SetPasswordStatement setPassword) { + visit(setPassword, null); + } + + public void visit(CreateEvent createEvent) { + visit(createEvent, null); + } + public void visit(CreateSequence createSequence) { visit(createSequence, null); } @@ -551,6 +987,11 @@ public void visit(Analyze analyze) { visit(analyze, null); } + @Override + public void visit(TableMaintenanceStatement statement) { + visit(statement, null); + } + public void visit(SavepointStatement savepointStatement) { visit(savepointStatement, null); } @@ -571,6 +1012,62 @@ public void visit(RenameTableStatement renameTableStatement) { visit(renameTableStatement, null); } + public void visit(PragmaStatement pragmaStatement) { + visit(pragmaStatement, null); + } + + public void visit(ExtensionStatement extensionStatement) { + visit(extensionStatement, null); + } + + public void visit(AttachStatement attachStatement) { + visit(attachStatement, null); + } + + public void visit(DetachStatement detachStatement) { + visit(detachStatement, null); + } + + public void visit(ConnectStatement connectStatement) { + visit(connectStatement, null); + } + + public void visit(DisconnectStatement disconnectStatement) { + visit(disconnectStatement, null); + } + + public void visit(PrepareStatement prepareStatement) { + visit(prepareStatement, null); + } + + public void visit(DeallocateStatement deallocateStatement) { + visit(deallocateStatement, null); + } + + public void visit(CopyStatement copyStatement) { + visit(copyStatement, null); + } + + public void visit(CreateMacro createMacro) { + visit(createMacro, null); + } + + public void visit(CreateExtensionRepository createExtensionRepository) { + visit(createExtensionRepository, null); + } + + public void visit(AssertStatement assertStatement) { + visit(assertStatement, null); + } + + public void visit(ExportDataStatement exportDataStatement) { + visit(exportDataStatement, null); + } + + public void visit(LoadDataStatement loadDataStatement) { + visit(loadDataStatement, null); + } + public void visit(PurgeStatement purgeStatement) { visit(purgeStatement, null); } @@ -593,11 +1090,337 @@ public void visit(Export export) { @Override public Void visit(CreatePolicy createPolicy, S context) { - // TODO: not yet implemented + validateOptionalFromItem(createPolicy.getTable()); + validateOptionalExpression(createPolicy.getUsingExpression()); + validateOptionalExpression(createPolicy.getWithCheckExpression()); return null; } public void visit(CreatePolicy createPolicy) { visit(createPolicy, null); } + + @Override + public Void visit(CreateRole statement, S context) { + validateFeature(Feature.createRole); + for (RoleOption option : statement.getOptions()) { + validateOptionalExpression(option.getValue()); + } + return null; + } + + @Override + public Void visit(AlterSchema statement, S context) { + validateFeature(Feature.alterSchema); + return null; + } + + @Override + public Void visit(AlterRole statement, S context) { + validateFeature(Feature.alterRole); + for (RoleOption option : statement.getOptions()) { + validateOptionalExpression(option.getValue()); + } + if (statement.getValues() != null) { + statement.getValues().forEach(this::validateOptionalExpression); + } + return null; + } + + @Override + public Void visit(Revoke statement, S context) { + validateFeature(Feature.revoke); + statement.getClause().visit(this::validateOptionalFromItem, + this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterDefaultPrivileges statement, S context) { + validateFeature(Feature.alterDefaultPrivileges); + if (statement.getGrant() != null) { + statement.getGrant().accept(this, context); + } + if (statement.getRevoke() != null) { + statement.getRevoke().accept(this, context); + } + return null; + } + + @Override + public Void visit(CreateType statement, S context) { + validateFeature(Feature.createType); + + return null; + } + + @Override + public Void visit(AlterType statement, S context) { + validateFeature(Feature.alterType); + + return null; + } + + @Override + public Void visit(CreateDomain statement, S context) { + validateFeature(Feature.createDomain); + validateOptionalExpression(statement.getDefaultExpression()); + statement.getConstraints() + .forEach(constraint -> validateOptionalExpression(constraint.getExpression())); + return null; + } + + @Override + public Void visit(AlterDomain statement, S context) { + validateFeature(Feature.alterDomain); + validateOptionalExpression(statement.getDefaultExpression()); + if (statement.getConstraint() != null) { + validateOptionalExpression(statement.getConstraint().getExpression()); + } + return null; + } + + @Override + public Void visit(CreateExtension statement, S context) { + validateFeature(Feature.createExtension); + + return null; + } + + @Override + public Void visit(AlterExtension statement, S context) { + validateFeature(Feature.alterExtension); + if (statement.getMember() != null && statement.getMember().isTable()) { + validateOptionalFromItem( + new Table(statement.getMember().getName())); + } + return null; + } + + @Override + public Void visit(CreatePublication statement, S context) { + validateFeature(Feature.createPublication); + statement.getTargets().forEach(target -> target.visit( + this::validateOptionalFromItem, this::validateOptionalExpression)); + return null; + } + + @Override + public Void visit(AlterPublication statement, S context) { + validateFeature(Feature.alterPublication); + statement.getTargets().forEach(target -> target.visit( + this::validateOptionalFromItem, this::validateOptionalExpression)); + return null; + } + + @Override + public Void visit(CreateSubscription statement, S context) { + validateFeature(Feature.createSubscription); + + return null; + } + + @Override + public Void visit(AlterSubscription statement, S context) { + validateFeature(Feature.alterSubscription); + + return null; + } + + @Override + public Void visit(OracleBlock block, S context) { + validateFeature(Feature.block); + validateFeature(Feature.oracleBlock); + block.visitChildren(this::validateOptionalExpression, + statement -> statement.accept(this, context)); + return null; + } + + @Override + public Void visit(OracleAssignment assignment, S context) { + validateFeature(Feature.oracleBlock); + validateOptionalExpression(assignment.getTarget()); + validateOptionalExpression(assignment.getValue()); + return null; + } + + @Override + public Void visit(OracleNullStatement statement, S context) { + validateFeature(Feature.oracleBlock); + return null; + } + + + @Override + public Void visit(CreateForeignDataWrapper statement, S context) { + validateFeature(Feature.createForeignDataWrapper); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterForeignDataWrapper statement, S context) { + validateFeature(Feature.alterForeignDataWrapper); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(CreateServer statement, S context) { + validateFeature(Feature.createServer); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterServer statement, S context) { + validateFeature(Feature.alterServer); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(CreateUserMapping statement, S context) { + validateFeature(Feature.createUserMapping); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterUserMapping statement, S context) { + validateFeature(Feature.alterUserMapping); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(DropUserMapping statement, S context) { + validateFeature(Feature.drop); + return null; + } + + @Override + public Void visit(CreateAccessMethod statement, S context) { + validateFeature(Feature.createAccessMethod); + if (statement.getType() == CreateAccessMethod.Type.TABLE) { + validateFeature(Feature.createAccessMethodTable); + } + return null; + } + + @Override + public Void visit(CreateTextSearchConfiguration statement, S context) { + validateFeature(Feature.createTextSearchConfiguration); + return null; + } + + @Override + public Void visit(AlterTextSearchConfiguration statement, S context) { + validateFeature(Feature.alterTextSearchConfiguration); + return null; + } + + @Override + public Void visit(CreateRule statement, S context) { + validateFeature(Feature.createRule); + statement.visitExpressions(this::validateOptionalExpression); + statement.visitTables(this::validateOptionalFromItem); + statement.getActions().forEach(action -> action.accept(this, context)); + return null; + } + + @Override + public Void visit(NotifyStatement statement, S context) { + validateFeature(Feature.notifyStatement); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(CreateCollation statement, S context) { + validateFeature(Feature.createCollation); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterCollation statement, S context) { + validateFeature(Feature.alterCollation); + return null; + } + + @Override + public Void visit(AlterRoutine statement, S context) { + switch (statement.getRoutineType()) { + case PROCEDURE: + validateFeature(Feature.alterProcedure); + break; + case ROUTINE: + validateFeature(Feature.alterRoutine); + break; + default: + validateFeature(Feature.alterFunction); + } + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterTrigger statement, S context) { + validateFeature(Feature.alterTrigger); + validateOptionalFromItem(statement.getTable()); + return null; + } + + @Override + public Void visit(AlterRelation statement, S context) { + boolean index = statement.getObjectType() == AlterRelation.ObjectType.INDEX; + validateFeature(index ? Feature.alterIndex : Feature.alterView); + if (!index) { + validateOptionalFromItem(statement.getRelation()); + } + statement.getActions().forEach(action -> { + action.visitExpressions(this::validateOptionalExpression); + action.visitTables(this::validateOptionalFromItem); + }); + return null; + } + + @Override + public Void visit(CreateTablespace statement, S context) { + validateFeature(Feature.createTablespace); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterTablespace statement, S context) { + validateFeature(Feature.alterTablespace); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterPolicy statement, S context) { + validateOptionalFromItem(statement.getTable()); + if (statement.getNewName() == null) { + statement.getOptions().visitExpressions(this::validateOptionalExpression); + } + return null; + } + + @Override + public Void visit(DropPolicy statement, S context) { + validateFeature(Feature.drop); + validateOptionalFromItem(statement.getTable()); + return null; + } + + @Override + public Void visit(CreateStatistics statement, S context) { + validateOptionalFromItem(statement.getTable()); + validateOptionalExpression(statement.getExpressions()); + return null; + } + } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/TableMaintenanceValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/TableMaintenanceValidator.java new file mode 100644 index 0000000000..7f022612e1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/TableMaintenanceValidator.java @@ -0,0 +1,26 @@ +/*- + * #%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.util.validation.validator; + +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.TableMaintenanceStatement; +import net.sf.jsqlparser.util.validation.ValidationCapability; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; + +public class TableMaintenanceValidator extends AbstractValidator { + @Override + public void validate(TableMaintenanceStatement statement) { + for (ValidationCapability c : getCapabilities()) { + validateFeature(c, Feature.tableMaintenance); + statement.getTables().forEach(table -> validateName(c, NamedObject.table, + table.getFullyQualifiedName(), true)); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java index 058b6aa9fc..b7695b3d42 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java @@ -10,7 +10,7 @@ package net.sf.jsqlparser.util.validation.validator; import net.sf.jsqlparser.parser.feature.Feature; -import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.util.validation.ValidationCapability; @@ -26,26 +26,20 @@ public void validate(Update update) { validateFeature(c, Feature.update); validateOptionalFeature(c, update.getFromItem(), Feature.updateFrom); validateOptionalFeature(c, update.getStartJoins(), Feature.updateJoins); - validateFeature(c, update.isUseSelect(), Feature.updateUseSelect); validateOptionalFeature(c, update.getOrderByElements(), Feature.updateOrderBy); validateOptionalFeature(c, update.getLimit(), Feature.updateLimit); validateOptionalFeature(c, update.getReturningClause(), Feature.updateReturning); } - validateOptionalFromItem(update.getTable()); + if (!update.isTargetTableAlias()) { + validateOptionalFromItem(update.getTable()); + } validateOptional(update.getStartJoins(), j -> getValidator(SelectValidator.class).validateOptionalJoins(j)); - if (update.isUseSelect()) { - validateOptionalExpressions(update.getColumns()); - validateOptional(update.getSelect(), - e -> e.accept((SelectVisitor) getValidator(SelectValidator.class), null)); - } else { - validateOptionalExpressions(update.getColumns()); - validateOptionalExpressions(update.getExpressions()); - } + validateOptionalUpdateSets(update.getUpdateSets()); if (update.getFromItem() != null) { validateOptionalFromItem(update.getFromItem()); @@ -53,6 +47,7 @@ public void validate(Update update) { j -> getValidator(SelectValidator.class).validateOptionalJoins(j)); } + validateOptionalForPortionClause(update.getForPortionClause()); validateOptionalExpression(update.getWhere()); validateOptionalOrderByElements(update.getOrderByElements()); @@ -60,6 +55,15 @@ public void validate(Update update) { getValidator(LimitValidator.class).validate(update.getLimit()); } + if (update.getOption() != null) { + for (OptionHint optionHint : update.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } + if (update.getReturningClause() != null) { SelectValidator v = getValidator(SelectValidator.class); update.getReturningClause().forEach(c -> c.accept(v, null)); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 9ac62f42c5..087c530b43 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -33,6 +33,10 @@ PARSER_BEGIN(CCJSqlParser) package net.sf.jsqlparser.parser; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + import java.lang.reflect.Field; import java.lang.Integer; @@ -45,35 +49,62 @@ import net.sf.jsqlparser.schema.*; import net.sf.jsqlparser.statement.*; import net.sf.jsqlparser.statement.analyze.*; import net.sf.jsqlparser.statement.alter.*; +import net.sf.jsqlparser.statement.create.fdw.*; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.sequence.*; import net.sf.jsqlparser.statement.comment.*; +import net.sf.jsqlparser.statement.create.database.*; +import net.sf.jsqlparser.statement.create.publication.*; +import net.sf.jsqlparser.statement.create.subscription.*; +import net.sf.jsqlparser.statement.create.type.*; +import net.sf.jsqlparser.statement.create.domain.*; +import net.sf.jsqlparser.statement.create.extension.*; +import net.sf.jsqlparser.statement.create.event.*; import net.sf.jsqlparser.statement.create.function.*; import net.sf.jsqlparser.statement.create.index.*; +import net.sf.jsqlparser.statement.create.macro.*; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.create.policy.*; import net.sf.jsqlparser.statement.create.procedure.*; import net.sf.jsqlparser.statement.create.schema.*; import net.sf.jsqlparser.statement.create.synonym.*; import net.sf.jsqlparser.statement.create.sequence.*; import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; +import net.sf.jsqlparser.statement.create.trigger.*; +import net.sf.jsqlparser.statement.create.user.*; import net.sf.jsqlparser.statement.create.view.*; import net.sf.jsqlparser.statement.delete.*; import net.sf.jsqlparser.statement.drop.*; import net.sf.jsqlparser.statement.insert.*; +import net.sf.jsqlparser.statement.load.*; import net.sf.jsqlparser.statement.execute.*; import net.sf.jsqlparser.statement.piped.*; import net.sf.jsqlparser.statement.select.*; import net.sf.jsqlparser.statement.refresh.*; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.show.*; import net.sf.jsqlparser.statement.truncate.*; import net.sf.jsqlparser.statement.update.*; import net.sf.jsqlparser.statement.upsert.*; import net.sf.jsqlparser.statement.merge.*; import net.sf.jsqlparser.statement.grant.*; +import net.sf.jsqlparser.statement.role.*; import net.sf.jsqlparser.statement.imprt.*; import net.sf.jsqlparser.statement.export.*; import net.sf.jsqlparser.statement.lock.*; import java.util.*; import java.util.AbstractMap.SimpleEntry; +import java.util.concurrent.CancellationException; import net.sf.jsqlparser.statement.select.SetOperationList.SetOperationType; import java.util.logging.Level; @@ -86,14 +117,21 @@ public class CCJSqlParser extends AbstractJSqlParser { public final static Logger LOGGER = Logger.getLogger(CCJSqlParser.class.getName()); public int bracketsCounter = 0; public int caseCounter = 0; - public boolean interrupted = false; + public volatile boolean interrupted = false; public CCJSqlParser withConfiguration(FeatureConfiguration configuration) { token_source.configuration = configuration; + jj_input_stream.featureConfiguration = configuration; return this; } public FeatureConfiguration getConfiguration() { + // wire the stream before parsing starts: every feature mutation goes + // through here (the consumers run pre-parse), default-off parsing + // never wires the stream and stays on the zero-cost path + if (jj_input_stream.featureConfiguration != token_source.configuration) { + jj_input_stream.featureConfiguration = token_source.configuration; + } return token_source.configuration; } @@ -101,15 +139,496 @@ public class CCJSqlParser extends AbstractJSqlParser { return this; } + // depth of open ClickHouse-style ternary then-branches: a ":" directly after the + // then-branch closes the ternary and must not be taken as the JSON path operator + private int ternaryThenBranchDepth = 0; + + // depth of open CREATE SCHEMA elements: GRANT there starts the next element, never an alias + private int schemaElementDepth = 0; + + // Keep this routing check aligned with SingleStatement, including aliases and dialect gates. + // It does not validate syntax that the grammar captures as opaque. + private boolean isStatementStartAhead() { + Token next = getToken(1); + switch (next.kind) { + case K_SELECT: case K_WITH: case K_VALUES: case K_VALUE: case K_FROM: + case K_PIVOT: case K_UNPIVOT: case OPENING_BRACKET: + case K_INSERT: case K_UPDATE: case K_DELETE: case K_MERGE: + case K_UPSERT: case K_REPLACE: case K_ALTER: case K_RENAME: + case K_CREATE: case K_DROP: case K_ANALYZE: case K_TRUNCATE: + case K_EXEC: case K_EXECUTE: case K_CALL: case K_SET: case K_RESET: + case K_SHOW: case K_REFRESH: case K_USE: case K_BEGIN: case K_START: + case K_SAVEPOINT: case K_ROLLBACK: case K_COMMIT: case K_COMMENT: + case K_DESCRIBE: case K_DESC: case K_EXPLAIN: case K_SUMMARIZE: case K_DECLARE: case K_GRANT: + case K_PRAGMA: case K_INSTALL: case K_LOAD: case K_FORCE: + case K_ATTACH: case K_DETACH: case K_CONNECT: case K_DISCONNECT: + case K_PREPARE: case K_DEALLOCATE: case K_COPY: case K_ASSERT: + case K_PURGE: case K_SESSION: case K_BRANCH: case K_LOCK: case K_IF: case K_TABLE: + return true; + case K_DO: + return Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)); + case K_SECURITY: + return "LABEL".equalsIgnoreCase(getToken(2).image); + case K_REINDEX: + return isReindexAhead(); + case K_IMPORT: case K_EXPORT: + return Dialect.EXASOL.name().equals(getAsString(Feature.dialect)) + || next.kind == K_EXPORT && getToken(2).kind == K_DATA; + default: + return "NOTIFY".equalsIgnoreCase(next.image) + || "REASSIGN".equalsIgnoreCase(next.image) + || "REVOKE".equalsIgnoreCase(next.image) + || "RELEASE".equalsIgnoreCase(next.image); + } + } + + private boolean isInformixIsolationLevel(Token token) { + return "DIRTY".equalsIgnoreCase(token.image) || "COMMITTED".equalsIgnoreCase(token.image) + || "CURSOR".equalsIgnoreCase(token.image) || token.kind == K_REPEATABLE; + } + + private boolean isOpaqueStatementAhead() { + int kind = getToken(1).kind; + return kind != EOF && kind != ST_SEMICOLON && kind != K_END && !isStatementStartAhead(); + } + + private void captureOpaqueStatements(Statements statements) throws ParseException { + while (getAsBoolean(Feature.allowUnsupportedStatements) && isOpaqueStatementAhead()) { + statements.add(UnsupportedStatement()); + while (getToken(1).kind == ST_SEMICOLON) { + getNextToken(); + } + } + } + + private void requireStatementBoundary() throws ParseException { + int kind = getToken(1).kind; + if (kind != EOF && kind != ST_SEMICOLON + && (kind == K_FROM || kind == K_PIVOT || kind == K_UNPIVOT || !isStatementStartAhead())) { + throw new ParseException("Unexpected token after a supported statement: " + getToken(1).image); + } + } + + + // depth of DuckDB MAP keys: the following ":" separates a key from its value + // and must not be taken as the JSON path operator + private int mapKeyDepth = 0; + + // opening brackets of the enclosing subscripts: a ":" at the top level of a subscript + // separates the bounds of a slice such as l[1:3] and is not the JSON path operator + private final Deque subscriptOpenings = new ArrayDeque(); + + /** + * Whether the pending ":" is at the top level of the innermost subscript, i.e. not nested + * in brackets, braces, parentheses or a CASE expression opened inside that subscript. + */ + private boolean isSliceColonAhead() { + Token colon = getToken(1); + return !subscriptOpenings.isEmpty() && ":".equals(colon.image) + && TokenNesting.isTopLevelFrom(subscriptOpenings.peek().next, colon); + } + + /** + * Token nesting checks shared by the lookaheads above and below. A nested class keeps its + * branches out of CCJSqlParser.class, whose constant pool is close to what JaCoCo can still + * instrument. + */ + private static final class TokenNesting { + private TokenNesting() {} + + /** + * Whether the target is not nested in brackets, braces, parentheses or a CASE expression + * opened from the first token on. + */ + static boolean isTopLevelFrom(Token first, Token target) { + int depth = 0; + for (Token t = first; t != null && t != target; t = t.next) { + if (t.kind == K_CASE || "(".equals(t.image) || "[".equals(t.image) + || "{".equals(t.image)) { + depth++; + } else if (t.kind == K_END || ")".equals(t.image) || "]".equals(t.image) + || "}".equals(t.image)) { + depth--; + } + } + return depth == 0; + } + } + + // first tokens of the enclosing column DEFAULT values: a COLLATE at their top level is the + // column's own attribute, as in MySQL's column_attribute and PostgreSQL's ColQualList + private final Deque columnDefaultStarts = new ArrayDeque(); + + /** Whether the pending COLLATE follows an unparenthesized column DEFAULT value. */ + private boolean isColumnCollateAhead() { + Token collate = getToken(1); + return !columnDefaultStarts.isEmpty() && collate.kind == K_COLLATE + && TokenNesting.isTopLevelFrom(columnDefaultStarts.peek(), collate); + } + + private Expression createOutputArgument(Expression argument) throws ParseException { + Expression value = argument; + if (argument instanceof VariableAssignment) { + value = ((VariableAssignment) argument).getExpression(); + } else if (argument instanceof EqualsTo + && ((EqualsTo) argument).getLeftExpression() instanceof UserVariable) { + value = ((EqualsTo) argument).getRightExpression(); + } + if (!(value instanceof UserVariable || value instanceof JdbcParameter + || value instanceof JdbcNamedParameter)) { + throw new ParseException("An OUTPUT argument must be a variable or bind parameter"); + } + return new net.sf.jsqlparser.statement.execute.ExecuteArgument(argument, true); + } + private void linkAST(ASTNodeAccess access, Node node) { access.setASTNode(node); node.jjtSetValue(access); } + /** + * Links a node whose production takes its left operand as a parameter, starting the node's + * token range at that operand rather than at the operator. The range is left as JJTree set it + * when the operand carries no node of its own. + */ + private void linkAST(ASTNodeAccess access, Node node, Expression leftExpression) { + linkAST(access, node); + node.jjtSetFirstToken(firstToken(leftExpression, node.jjtGetFirstToken())); + } + + /** + * Links an object no production opens a node for to a {@link RangeNode} spanning its tokens + * from {@code first} to the last one consumed. + */ + private void linkAST(ASTNodeAccess access, Token first) { + linkAST(access, first, getToken(0)); + } + + private void linkAST(ASTNodeAccess access, Token first, Token last) { + linkAST(access, new RangeNode(first, last, null)); + } + + private static Token firstToken(ASTNodeAccess operand, Token fallback) { + Node node = operand == null ? null : operand.getASTNode(); + return node != null && node.jjtGetFirstToken() != null ? node.jjtGetFirstToken() : fallback; + } + + /** Links a list the grammar assembles itself to the range from its first element to its last. */ + private > T linkElements(T list) { + if (list != null && !list.isEmpty() && list.getASTNode() == null) { + Node first = list.get(0).getASTNode(); + Node last = list.get(list.size() - 1).getASTNode(); + if (first != null && last != null) { + linkAST(list, first.jjtGetFirstToken(), last.jjtGetLastToken()); + } + } + return list; + } + + /** Links an object built from the single token just consumed, such as a name, to that token. */ + private T linkToLastToken(T access) { + linkAST(access, getToken(0), getToken(0)); + return access; + } + + private T linked(T access, Token first) { + linkAST(access, first); + return access; + } + + /** + * Widens an object's range to text it has taken in beyond its node, from {@code first} or to + * {@code last} where given. A tree node keeps its own range, and the object moves to a + * {@link RangeNode} over the wider one. + */ + private void widen(Object access, Token first, Token last) { + Node node = access instanceof ASTNodeAccess ? ((ASTNodeAccess) access).getASTNode() : null; + if (node == null) { + return; + } + Token from = first != null ? first : node.jjtGetFirstToken(); + Token to = last != null ? last : node.jjtGetLastToken(); + if (node instanceof RangeNode) { + node.jjtSetFirstToken(from); + node.jjtSetLastToken(to); + } else { + linkAST((ASTNodeAccess) access, new RangeNode(from, to, node)); + } + } + + /** Links a statement that no other rule linked to the range from {@code first} on. */ + private void linkStatement(Statement statement, Token first) { + if (statement instanceof ASTNodeAccess) { + linkASTIfMissing((ASTNodeAccess) statement, first); + } + } + + /** + * The last token of the qualifier in a dotted name running from {@code first} to {@code name}, + * which is the token two before {@code name}. Null when the name has no qualifier. + */ + private static Token qualifierLast(Token first, Token name) { + for (Token token = first; token != null && token != name; token = token.next) { + if (token.next != null && token.next.next == name) { + return token; + } + } + return null; + } + + /** Like {@link #linkAST(ASTNodeAccess, Token)}, for an object a production may already have linked. */ + private void linkASTIfMissing(ASTNodeAccess access, Token first) { + if (access != null && access.getASTNode() == null) { + linkAST(access, first); + } + } + public Node getASTRoot() { return jjtree.rootNode(); } + private boolean isMySqlDatabaseOptionAhead() { + return isMySqlDatabaseOptionAhead(1); + } + + private boolean isMySqlDatabaseOptionAhead(int offset) { + int start = getToken(offset).kind == K_DEFAULT ? offset + 1 : offset; + Token token = getToken(start); + return token.kind == K_CHARACTER || token.kind == K_COLLATE || token.kind == K_ENCRYPTION + || token.kind == K_CHAR && getToken(start + 1).kind == K_SET + || token.kind == S_IDENTIFIER && "CHARSET".equalsIgnoreCase(token.image); + } + + /** + * Whether MySQL's ALTER DATABASE omits the database name before its options. CHARSET can also + * be a database name, so it starts an option only when "=" follows, or when its value ends the + * statement or precedes another option. + */ + private boolean isAlterDatabaseNameOmitted() { + Token first = getToken(1); + if (first.kind == K_READ) { + return true; + } + if (!isMySqlDatabaseOptionAhead(1)) { + return false; + } + if (first.kind != S_IDENTIFIER || "=".equals(getToken(2).image)) { + return true; + } + Token afterValue = getToken(3); + return afterValue.kind == EOF || afterValue.kind == ST_SEMICOLON || afterValue.kind == K_READ + || isMySqlDatabaseOptionAhead(3); + } + + /** Whether a PostgreSQL CREATE/ALTER DATABASE option name starts at the offset. */ + private boolean isPostgreSqlDatabaseOptionAhead(int offset) { + Token token = getToken(offset); + if (token.kind == K_ENCODING || token.kind == K_TABLESPACE) { + return true; + } + if (token.kind != S_IDENTIFIER) { + return false; + } + String name = token.image.toUpperCase(Locale.ROOT); + if ("CONNECTION".equals(name)) { + return getToken(offset + 1).kind == K_LIMIT; + } + return Arrays.asList("OWNER", "TEMPLATE", "STRATEGY", "LOCALE", "LC_COLLATE", "LC_CTYPE", + "BUILTIN_LOCALE", "ICU_LOCALE", "ICU_RULES", "LOCALE_PROVIDER", "COLLATION_VERSION", + "ALLOW_CONNECTIONS", "IS_TEMPLATE", "OID").contains(name); + } + + /** Whether the words, matched without case, start at the offset. */ + private boolean isWordSequenceAhead(int offset, String... words) { + for (int i = 0; i < words.length; i++) { + Token token = getToken(offset + i); + if (token.kind == EOF || !words[i].equalsIgnoreCase(token.image)) { + return false; + } + } + return true; + } + + private boolean isSignedNumberAhead(int offset) { + Token token = getToken(offset); + if ("+".equals(token.image) || "-".equals(token.image)) { + token = getToken(offset + 1); + } + return token.kind == S_LONG || token.kind == S_DOUBLE; + } + + /** + * Whether ALTER FUNCTION, PROCEDURE or ROUTINE has a PostgreSQL or MySQL alteration after the + * routine name and optional signature. Other forms, such as SQL Server routine definitions, + * remain unsupported statements. + */ + private boolean isAlterRoutineAhead() { + Token token = getToken(1); + if (token.kind != K_FUNCTION && token.kind != K_PROCEDURE && !isKeywordAhead("ROUTINE")) { + return false; + } + int offset = 3; + while (".".equals(getToken(offset).image)) { + offset += 2; + } + if (getToken(offset).kind == OPENING_BRACKET) { + for (int depth = 0; ; offset++) { + int kind = getToken(offset).kind; + if (kind == EOF || kind == ST_SEMICOLON) { + return false; + } + depth += kind == OPENING_BRACKET ? 1 : kind == CLOSING_BRACKET ? -1 : 0; + if (depth == 0) { + offset++; + break; + } + } + } + Token next = getToken(offset + 1); + return getToken(offset).kind == K_RENAME && next.kind == K_TO + || isWordSequenceAhead(offset, "OWNER", "TO") + || getToken(offset).kind == K_SET && next.kind == K_SCHEMA + || isExtensionDependencyAhead(offset) + || isPostgreSqlRoutineCharacteristicAhead(offset) + || isMySqlRoutineCharacteristicAhead(offset); + } + + /** Whether PostgreSQL's [NO] DEPENDS ON EXTENSION starts at the offset. */ + private boolean isExtensionDependencyAhead(int offset) { + return isWordSequenceAhead(getToken(offset).kind == K_NO ? offset + 1 : offset, + "DEPENDS", "ON", "EXTENSION"); + } + + /** + * Whether a PostgreSQL characteristic of ALTER FUNCTION, PROCEDURE or ROUTINE starts at the + * offset. SET without TO, = or FROM after the parameter name is SET SCHEMA instead. + */ + private boolean isPostgreSqlRoutineCharacteristicAhead(int offset) { + Token token = getToken(offset); + switch (token.kind) { + case K_SET: + Token assignment = getToken(offset + 2); + while (".".equals(assignment.image)) { + assignment = nextWithItemToken(nextWithItemToken(assignment)); + } + return assignment.kind == K_TO || "=".equals(assignment.image) + || assignment.kind == K_FROM; + case K_RESET: + case K_VOLATILE: + case K_STRICT: + return true; + case K_ROWS: + return isSignedNumberAhead(offset + 1); + case K_NOT: + return isWordSequenceAhead(offset + 1, "LEAKPROOF"); + case K_RETURNS: + return isWordSequenceAhead(offset + 1, "NULL", "ON", "NULL", "INPUT"); + case K_PARALLEL: + return isWordSequenceAhead(offset + 1, "UNSAFE") + || isWordSequenceAhead(offset + 1, "RESTRICTED") + || isWordSequenceAhead(offset + 1, "SAFE"); + case K_SECURITY: + return isRoutineSecurityAhead(offset + 1); + case S_IDENTIFIER: + break; + default: + return false; + } + switch (token.image.toUpperCase(Locale.ROOT)) { + case "IMMUTABLE": + case "STABLE": + case "LEAKPROOF": + return true; + case "CALLED": + return isWordSequenceAhead(offset + 1, "ON", "NULL", "INPUT"); + case "EXTERNAL": + return getToken(offset + 1).kind == K_SECURITY && isRoutineSecurityAhead(offset + 2); + case "COST": + return isSignedNumberAhead(offset + 1); + case "SUPPORT": + int kind = getToken(offset + 1).kind; + return kind != EOF && kind != ST_SEMICOLON; + default: + return false; + } + } + + /** Whether a MySQL characteristic of CREATE FUNCTION or PROCEDURE starts here. */ + private boolean isMySqlCreateRoutineCharacteristicAhead() { + return isMySqlRoutineCharacteristicAhead(1) || isWordSequenceAhead(1, "DETERMINISTIC") + || isWordSequenceAhead(1, "NOT", "DETERMINISTIC"); + } + + /** Whether a MySQL characteristic of ALTER FUNCTION or PROCEDURE starts at the offset. */ + private boolean isMySqlRoutineCharacteristicAhead(int offset) { + if (getToken(offset).kind == K_COMMENT) { + return getToken(offset + 1).kind == S_CHAR_LITERAL; + } + return isWordSequenceAhead(offset, "LANGUAGE", "SQL") + || isWordSequenceAhead(offset, "CONTAINS", "SQL") + || isWordSequenceAhead(offset, "NO", "SQL") + || isWordSequenceAhead(offset, "READS", "SQL", "DATA") + || isWordSequenceAhead(offset, "MODIFIES", "SQL", "DATA") + || isWordSequenceAhead(offset, "SQL", "SECURITY") && isRoutineSecurityAhead(offset + 2); + } + + private boolean isRoutineSecurityAhead(int offset) { + return isWordSequenceAhead(offset, "DEFINER") || isWordSequenceAhead(offset, "INVOKER"); + } + + /** + * Whether a MySQL compound statement starts here: BEGIN ... END or a flow control statement, + * where BEGIN, LOOP, REPEAT and WHILE may have a label. + */ + private boolean isMySqlCompoundStatementAhead() { + boolean labeled = getToken(1).kind == S_IDENTIFIER && ":".equals(getToken(2).image); + Token token = getToken(labeled ? 3 : 1); + if (token.kind == K_BEGIN || token.kind == K_LOOP + || isWordSequenceAhead(labeled ? 3 : 1, "REPEAT") + || isWordSequenceAhead(labeled ? 3 : 1, "WHILE")) { + return true; + } + return !labeled && (token.kind == K_IF || token.kind == K_CASE); + } + + /** + * Whether BEGIN ... END holds only SQL statements, each ending with a semicolon, and ends the + * enclosing statement. CASE ... END inside a statement is an expression. + */ + private boolean isStatementBlockAhead() { + if (getToken(1).kind != K_BEGIN) { + return false; + } + boolean statementStart = true; + boolean statements = false; + int caseDepth = 0; + for (Token token = getToken(2); ; token = nextWithItemToken(token)) { + if (token.kind == EOF) { + return false; + } else if (token.kind == ST_SEMICOLON) { + statementStart = true; + } else if (statementStart) { + if (token.kind == K_END) { + int next = nextWithItemToken(token).kind; + return statements && (next == ST_SEMICOLON || next == EOF); + } + switch (token.kind) { + case K_SELECT: case K_INSERT: case K_UPDATE: case K_DELETE: case K_REPLACE: + case K_SET: case K_CALL: case K_WITH: + break; + default: + return false; + } + statementStart = false; + statements = true; + } else if (token.kind == K_CASE) { + caseDepth++; + } else if (token.kind == K_END && caseDepth-- == 0) { + return false; + } + } + } + private static class ObjectNames { private final List names; @@ -182,12 +701,23 @@ public class CCJSqlParser extends AbstractJSqlParser { // comparison operator follows after "(" "+" ")" return isComparisonOperator(getToken(4)); } + if ("?".equals(token.image) && isTernaryAhead()) { + // ClickHouse ternary `cond ? then : else` — the `?` must not be taken + // as the PostgreSQL JSON operator here, it is consumed by prattExpressionRest + return false; + } return isComparisonOperator(token); } catch (Exception e) { return false; } } + /** DuckDB's and PostgreSQL's {@code ~~}, {@code ~~*}, {@code !~~}, {@code !~~*} and {@code ~~~}. */ + protected static boolean isLikeOperator(Token token) { + return token.image != null + && (token.image.startsWith("~~") || token.image.startsWith("!~~")); + } + protected static boolean isComparisonOperator(Token token) { if (token.image == null || token.image.isEmpty()) { return false; @@ -196,12 +726,13 @@ public class CCJSqlParser extends AbstractJSqlParser { case '>': // >, >= case '=': // =, =* but not => Oracle/PostgreSQL named parameter syntax return !token.image.equals("=>"); - case '~': // ~, ~* - return true; + case '~': // ~, ~* but not the ~~ LIKE operators + return !token.image.startsWith("~~"); case '<': // <, <=, <>, <@, <->, <#>, <=>, <& return true; case '*': return token.image.equals("*="); - case '!': return token.image.startsWith("!~") || token.image.startsWith("!="); + case '!': return (token.image.startsWith("!~") && !token.image.startsWith("!~~")) + || token.image.startsWith("!="); case '@': return token.image.equals("@@") || token.image.equals("@>"); case '?': return true; // ?, ?|, ?& case '-': return true; // -, -# @@ -212,6 +743,97 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * True when the pending "?" starts a ClickHouse-style ternary conditional + * {@code cond ? then : else} rather than the PostgreSQL JSON operator: + * a standalone ":" closes the then-branch at the same nesting depth before + * any expression boundary (",", ";", EOF, an unbalanced closing bracket or a + * clause keyword such as FROM/WHERE). A ":" in operand position (directly + * after an operator) starts a JDBC named parameter instead and cannot close + * the then-branch. + */ + protected boolean isTernaryAhead() { + try { + int depth = 0; + Token prev = null; + for (Token t = getToken(2); ; t = nextWithItemToken(t)) { + if (t == null || t.kind == EOF) { + return false; + } + String image = t.image; + if ("(".equals(image) || "[".equals(image)) { + depth++; + } else if (")".equals(image) || "]".equals(image)) { + if (depth == 0) { + return false; + } + depth--; + } else if (depth == 0) { + if (":".equals(image) && canEndExpression(prev)) { + return true; + } + if (",".equals(image) || ";".equals(image)) { + return false; + } + switch (t.kind) { + case K_SELECT: case K_FROM: case K_WHERE: case K_GROUP: + case K_HAVING: case K_ORDER: case K_LIMIT: case K_UNION: + case K_INTERSECT: case K_EXCEPT: case K_MINUS: case K_INTO: + case K_VALUES: case K_FETCH: case K_OFFSET: + return false; + default: + break; + } + } + prev = t; + } + } catch (Exception e) { + return false; + } + } + + /** + * True when the token can be the LAST token of an expression: an + * identifier, a literal, a JDBC parameter, or a balanced closing bracket. + * Only such a token may directly precede the ":" closing the ternary + * then-branch, since the then-branch must be a complete expression. A ":" + * in operand position instead (directly after an operator, e.g. `x = :name` + * or directly after the leading "?") starts a JDBC named parameter. + * DATA_TYPE tokens end expressions as well, because a cast's target type + * (`b :: int`) or a bare type keyword closes the then-branch. The bare "?" + * positional parameter has no token-kind constant (anonymous token), so it + * is recognized by its image, like the closing brackets. + */ + private boolean canEndExpression(Token t) { + if (t == null) { + return false; + } + switch (t.kind) { + case S_IDENTIFIER: case S_QUOTED_IDENTIFIER: case S_PARAMETER: + case S_LONG: case S_DOUBLE: case S_HEX: case S_CHAR_LITERAL: + case K_DATE_LITERAL: case K_DATETIMELITERAL: case DATA_TYPE: + case K_NULL: case K_TRUE: case K_FALSE: + return true; + default: + return (t.kind >= MIN_NON_RESERVED_WORD && t.kind <= MAX_NON_RESERVED_WORD) + || ")".equals(t.image) || "]".equals(t.image) + || "?".equals(t.image); + } + } + + /** + * Whether the next two tokens form a character-set-introduced string literal, + * for example {@code _utf8mb4'text'} or {@code _ISO8859_1 'text'}. + */ + private boolean isCharacterSetIntroducedStringAhead() { + Token introducer = getToken(1); + Token literal = getToken(2); + return introducer.kind == S_IDENTIFIER + && introducer.image != null + && introducer.image.length() > 1 + && introducer.image.charAt(0) == '_' + && literal.kind == S_CHAR_LITERAL; + } /** * Tokens that have dedicated branches in PrimaryExpression AFTER the Function branch. @@ -228,69 +850,138 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * Whether PRIOR starts Oracle's hierarchical operator, as in PRIOR id or PRIOR (a), rather + * than naming a column, as in prior = 1, prior IS NULL or ORDER BY prior DESC. + */ + private boolean isPriorOperatorAhead() { + if (getToken(1).kind != K_PRIOR) { + return false; + } + int kind = getToken(2).kind; + switch (kind) { + case OPENING_BRACKET: case S_IDENTIFIER: case S_QUOTED_IDENTIFIER: case DATA_TYPE: + case S_LONG: case S_DOUBLE: case S_CHAR_LITERAL: + return true; + case K_ASC: case K_DESC: case K_NULLS: + return false; + default: + return kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD + && !isConditionKeywordAhead(2); + } + } + /** * Scans ahead through a dotted identifier chain and checks if '(' follows. * Distinguishes function calls like func(), schema.func(), a.b.c.func() * from column references like col, schema.col, a.b.c.col. * * Replaces LOOKAHEAD(16) on Function() with a targeted O(chain-length) check. + * Long chains are memoized per start token: the phase-2 lookahead routines + * re-evaluate the same walk many times per position and each chain part + * would walk the very same suffix again, which makes long chains parse in + * quadratic time. Chains up to CHAIN_CACHE_THRESHOLD parts skip the cache + * entirely, keeping the normal short-name path allocation- and map-free. */ + private static final int CHAIN_CACHE_THRESHOLD = 8; + + // Sentinel returned by the chain walks when the non-filling pass ran past + // CHAIN_CACHE_THRESHOLD delimiter/part pairs and the caller must retry + // through the per-token cache. + private static final int LONG_CHAIN = -1; + + private final Map isFunctionAheadCache = new HashMap(); + protected boolean isFunctionAhead() { + int r = isFunctionAheadWalk(false); + if (r != LONG_CHAIN) { + return r == 1; + } + Token key = getToken(1); + Boolean cached = isFunctionAheadCache.get(key); + if (cached != null) { + return cached; + } + boolean result = isFunctionAheadWalk(true) == 1; + isFunctionAheadCache.put(key, result); + return result; + } + + // True when a fresh evaluation started at this token would pass all + // first-token guards and run the same chain walk. + private boolean isFunctionAheadChainStartEligible(Token t) { + return !t.image.equals("{") && !t.image.equals("[") && t.kind != K_APPROXIMATE && !isNonFunctionKeyword(t) + && t.kind != S_LONG && t.kind != S_DOUBLE && t.kind != S_HEX + && t.kind != S_CHAR_LITERAL && t.kind != OPENING_BRACKET + && t.kind != CLOSING_BRACKET && t.kind != EOF; + } + + // Returns 1 for true, 0 for false, or LONG_CHAIN when the walk exceeded + // CHAIN_CACHE_THRESHOLD delimiter/part pairs (only in the non-filling pass). + private int isFunctionAheadWalk(boolean fill) { try { - int i = 1; - Token t = getToken(i); + Token t = getToken(1); // JDBC escape function: {fn ...} — must check for FN keyword if (t.image.equals("{")) { - return getToken(2).kind == K_FN; + return getToken(2).kind == K_FN ? 1 : 0; } // Optional APPROXIMATE keyword if (t.kind == K_APPROXIMATE) { - i++; - t = getToken(i); + t = getToken(2); } // Exclude tokens that have their own dedicated branches // after Function() in PrimaryExpression if (isNonFunctionKeyword(t)) { - return false; + return 0; } // First token must not be a literal, bracket, or EOF if (t.kind == S_LONG || t.kind == S_DOUBLE || t.kind == S_HEX || t.kind == S_CHAR_LITERAL || t.kind == OPENING_BRACKET - || t.kind == CLOSING_BRACKET || t.kind == EOF) { - return false; + || t.kind == CLOSING_BRACKET || t.kind == EOF || "[".equals(t.image)) { + return 0; } - i++; - - // Walk through dotted name chain + // Walk through dotted name chain, following the token chain directly; + // repeated getToken(i) calls rescan from the start each time + int delimiters = 0; + List chainParts = fill ? new ArrayList() : null; + t = nextWithItemToken(t); while (true) { - t = getToken(i); if (t.image.equals(".") || t.image.equals("..") || t.image.equals("...") || t.image.equals(":")) { - i++; // skip delimiter - i++; // skip next name part + if (!fill && ++delimiters > CHAIN_CACHE_THRESHOLD) { + return LONG_CHAIN; + } + Token part = nextWithItemToken(t); // skip delimiter + if (fill) { + chainParts.add(part); + } + t = nextWithItemToken(part); // skip next name part } else { break; } } - // Must be followed by ( - if (getToken(i).kind != OPENING_BRACKET) { - return false; - } - - // Exclude Oracle join syntax: column(+) - if (getToken(i + 1).image.equals("+") - && getToken(i + 2).kind == CLOSING_BRACKET) { - return false; + // Must be followed by (, and not by the Oracle join syntax column(+) + boolean result = t.kind == OPENING_BRACKET + && !(nextWithItemToken(t).image.equals("+") + && nextWithItemToken(nextWithItemToken(t)).kind == CLOSING_BRACKET); + + // Each eligible chain part would walk the same remaining suffix and + // obtain the same answer, so memoize them all in one pass. + if (fill) { + for (Token part : chainParts) { + if (isFunctionAheadChainStartEligible(part)) { + isFunctionAheadCache.put(part, result); + } + } } - - return true; + return result ? 1 : 0; } catch (TokenMgrException e) { - return false; + return 0; } } @@ -316,27 +1007,58 @@ public class CCJSqlParser extends AbstractJSqlParser { /** * Pratt arithmetic operator precedence loop. - * Handles: *, /, %, ^, DIV (prec=6) and +, -, ||, |, &, <<, >> (prec=5) + * Handles: ^, ** (prec=7), *, /, %, DIV (prec=6), +, -, || (prec=5) and |, &, #, <<, >> (prec=4) * Called only in real-parse mode (not syntactic-LOOKAHEAD mode) so action * blocks execute correctly. Safe now that no syntactic production LOOKAHEADs * scan through arithmetic expressions. */ /** * Pratt boolean operator precedence loop. - * Handles OR (prec=2), XOR (prec=3), AND/&& (prec=4). + * Handles OR (prec=2), XOR (prec=3 under MySQL, else 2), AND/&& (prec=4). * Safe to use now that no syntactic production LOOKAHEADs scan through expressions. */ - protected Expression prattExpressionRest(Expression left, int minPrec) throws ParseException { + protected Expression prattExpressionRest(Expression left, Token first, int minPrec) throws ParseException { while (!interrupted) { - int op = getToken(1).kind; + Token t = getToken(1); + int op = t.kind; + boolean ternary = "?".equals(t.image) && isTernaryAhead(); int prec; if (op == K_AND || op == OP_DOUBLEAND) prec = 4; - else if (op == K_XOR || op == K_OR) prec = 2; + // MySQL puts XOR between AND and OR; other dialects keep one + // left-associative level for OR and XOR. + else if (op == K_XOR) + prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 3 : 2; + else if (op == K_OR) prec = 2; + else if (ternary) prec = 2; else break; if (prec < minPrec) break; + if (ternary) { + // ClickHouse-style ternary conditional: cond ? then : else + // Right-associative: both branches parse at the full expression + // level (prec 2), nested ternaries are absorbed by the else-branch. + jj_consume_token(op, t.image); + Expression thenExpression; + ternaryThenBranchDepth++; + try { + Token thenFirst = getToken(1); + thenExpression = prattExpressionRest(Condition(), thenFirst, 2); + } finally { + ternaryThenBranchDepth--; + } + if (!":".equals(getToken(1).image)) { + throw new ParseException("Expected ':' closing the ternary conditional operator"); + } + jj_consume_token(getToken(1).kind, getToken(1).image); + Token elseFirst = getToken(1); + Expression elseExpression = prattExpressionRest(Condition(), elseFirst, 2); + left = new TernaryExpression(left, thenExpression, elseExpression); + linkAST(left, first); + continue; + } jj_consume_token(op, getToken(1).image); // +1 makes OR/AND/XOR left-associative - Expression right = prattExpressionRest(Condition(), prec + 1); + Token rightFirst = getToken(1); + Expression right = prattExpressionRest(Condition(), rightFirst, prec + 1); if (op == K_AND) { left = new AndExpression(left, right); } else if (op == OP_DOUBLEAND) { @@ -347,27 +1069,66 @@ public class CCJSqlParser extends AbstractJSqlParser { } else { left = new OrExpression(left, right); } + linkAST(left, first); } return left; } - protected Expression prattArithRest(Expression left, int minPrec) throws ParseException { + private boolean isPostgreSqlJsonAccess(Token token) { + return ("->".equals(token.image) || "->>".equals(token.image) + || "#>".equals(token.image) || "#>>".equals(token.image)) + && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)); + } + + /** + * Whether the ABSENT ON NULL or RETURNING clause of JSON_ARRAY or JSON_OBJECT starts here, + * rather than an element: ABSENT is a non-reserved word and RETURNING may name a column. + */ + private boolean isJsonConstructorClauseAhead() { + int kind = getToken(1).kind; + return kind == K_RETURNING || kind == K_ABSENT && getToken(2).kind == K_ON; + } + + protected Expression prattArithRest(Expression left, Token first, int minPrec) throws ParseException { while (!interrupted) { Token t = getToken(1); int op = t.kind; int prec; + boolean jsonAccess = isPostgreSqlJsonAccess(t); // Named tokens: OP_SLASH(/), OP_CARET(^), K_DIV, OP_CONCAT(||), - // OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>) + // OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>), + // S_HASH_OPERATOR(#) // String-literal tokens: *, +, -, %, & (unnamed in JavaCC grammar) - if (op == OP_SLASH || op == OP_CARET || op == K_DIV) prec = 6; - else if (op == OP_CONCAT || op == OP_PIPE - || op == OP_LSHIFT || op == OP_RSHIFT) prec = 5; + if (jsonAccess || op == OP_JSON_PATH_DELETE || op == OP_JSON_PATH_EXISTS + || (op == OP_CONCAT && Dialect.POSTGRESQL.name() + .equals(getAsString(Feature.dialect)))) prec = 4; + else if (op == OP_SLASH || op == K_DIV) prec = 6; + // ^ binds tighter than * / %, both as PostgreSQL's power and as MySQL's XOR + else if (op == OP_CARET || op == OP_POWER) prec = 7; + else if (op == OP_CONCAT + || (op == OP_PIPE && getToken(2).kind == OP_PIPE)) prec = 5; + // the bitwise operators bind looser than + and -, as in PostgreSQL, MySQL and SQLite + else if (op == S_HASH_OPERATOR || op == OP_LSHIFT + || op == OP_RSHIFT) prec = 4; + // PostgreSQL distance operators share the left-associative tier below + and -. + else if (op == OP_COSINESIMILARITY) prec = 4; + else if (op == OP_PIPE) { + // MySQL tiers bitwise: <<, >> above & above |. Other dialects + // (PostgreSQL, SQLite, ...) keep one left-associative level. + prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 2 : 4; + } else { // Handle unnamed tokens by image String img = t.image; + // DuckDB's LIMIT 10%: the % ends the row count instead of starting a modulo + if (t == limitPercentToken) break; if ("*".equals(img) || "%".equals(img)) prec = 6; - else if ("+".equals(img) || "-".equals(img) || "&".equals(img)) prec = 5; + else if ("+".equals(img) || "-".equals(img)) prec = 5; + else if ("&".equals(img)) { + prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 3 : 4; + } + else if ("<->".equals(img) || "<#>".equals(img)) prec = 4; else break; // not an arithmetic operator } @@ -377,21 +1138,38 @@ public class CCJSqlParser extends AbstractJSqlParser { if (op == OP_PIPE && getToken(2).kind == OP_PIPE) { jj_consume_token(OP_PIPE, getToken(1).image); jj_consume_token(OP_PIPE, getToken(1).image); - Expression right = prattArithRest(PrimaryExpression(), prec + 1); + Token rightFirst = getToken(1); + Expression right = prattArithRest(PrimaryExpression(), rightFirst, prec + 1); Concat r = new Concat(); r.setLeftExpression(left); r.setRightExpression(right); left = r; + linkAST(left, first); continue; } jj_consume_token(op, getToken(1).image); - Expression right = prattArithRest(PrimaryExpression(), prec + 1); - - if (op == OP_SLASH) { Division r = new Division(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } - else if (op == OP_CARET) { net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + Token rightFirst = getToken(1); + Expression right = prattArithRest(PrimaryExpression(), rightFirst, prec + 1); + // a JSON access chain extends the JsonExpression it started, which already has a node + boolean extended = jsonAccess && left instanceof JsonExpression; + + if (jsonAccess) { + JsonExpression access = left instanceof JsonExpression + ? (JsonExpression) left : new JsonExpression(left); + access.addIdent(right, t.image); + left = access; + } + else if (op == OP_JSON_PATH_DELETE || op == OP_JSON_PATH_EXISTS) { + left = new JsonOperator(t.image).withLeftExpression(left).withRightExpression(right); + } + else if (op == OP_SLASH) { Division r = new Division(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == S_HASH_OPERATOR) { Intersects r = new Intersects(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == OP_CARET) { net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == K_DIV) { IntegerDivision r = new IntegerDivision(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == OP_POWER) { left = new Exponentiation(left, right); } else if (op == OP_CONCAT) { Concat r = new Concat(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == OP_PIPE) { BitwiseOr r = new BitwiseOr(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == OP_LSHIFT) { BitwiseLeftShift r = new BitwiseLeftShift(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == OP_RSHIFT) { BitwiseRightShift r = new BitwiseRightShift(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == OP_COSINESIMILARITY) { CosineSimilarity r = new CosineSimilarity(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else { String img = t.image; if ("*".equals(img)) { Multiplication r = new Multiplication(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } @@ -399,6 +1177,12 @@ public class CCJSqlParser extends AbstractJSqlParser { else if ("-".equals(img)) { Subtraction r = new Subtraction(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if ("%".equals(img)) { Modulo r = new Modulo(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if ("&".equals(img)) { BitwiseAnd r = new BitwiseAnd(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if ("<->".equals(img) || "<#>".equals(img)) { GeometryDistance r = new GeometryDistance(img); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + } + if (extended) { + widen(left, first, getToken(0)); + } else { + linkAST(left, first); } } return left; @@ -411,10 +1195,11 @@ public class CCJSqlParser extends AbstractJSqlParser { try { if (getToken(1).kind != OPENING_BRACKET) return false; int k2 = getToken(2).kind; - // Direct subquery: ( SELECT/WITH ) → ParenthesedSelect handles it + // Direct subquery: ( SELECT/WITH/PIVOT ) → ParenthesedSelect handles it // ( VALUES ) can be ParenthesedFromItem(Values) or ParenthesedSelect; // ParenthesedFromItem is checked first so Values-as-FROM-item works - if (k2 == K_SELECT || k2 == K_WITH) return false; + if (k2 == K_SELECT || k2 == K_WITH || k2 == K_PIVOT || k2 == K_FROM) return false; + if (k2 == K_TABLE && isTableQueryAhead(2)) return false; // ( ( SELECT ) UNION ( SELECT ) ) → isNestedSetOperationAhead handles it // Other (( patterns: ( (SELECT...) JOIN ... ) or ( (table) ) → ParenthesedFromItem if (k2 == K_LATERAL) return false; // handled by LateralSubSelect @@ -434,38 +1219,131 @@ public class CCJSqlParser extends AbstractJSqlParser { if (getToken(1).kind != OPENING_BRACKET) return false; if (getToken(2).kind != OPENING_BRACKET) return false; // Skip any run of opening brackets: ( ( ( ... ( SELECT - int i = 3; - while (getToken(i).kind == OPENING_BRACKET) i++; - int k = getToken(i).kind; - if (k != K_SELECT && k != K_WITH && k != K_VALUES) return false; - // Scan forward counting brackets to find the matching ) for token(2) - // We are now inside all the opening brackets; depth = number of brackets skipped - 1 - // (token(1) is the outer one, token(2..i-1) are the inner ones we scanned) - int depth = i - 2; // brackets from position 2 to i-1 inclusive - i++; - while (i <= 500) { - Token t; try { t = getToken(i); } catch (TokenMgrException e) { return false; } - if (t == null || t.kind == 0) return false; - if (t.kind == OPENING_BRACKET) depth++; - else if (t.kind == CLOSING_BRACKET) { - if (--depth == 0) break; - } - i++; - } - if (i > 500) return false; - // Token at position i+1 follows the innermost closing ) - Token next; try { next = getToken(i + 1); } catch (TokenMgrException e) { return false; } + Token innerOpen = getToken(2); + Token t = innerOpen; + while (t.kind == OPENING_BRACKET) t = nextWithItemToken(t); + if (t.kind != K_SELECT && t.kind != K_WITH && t.kind != K_VALUES) return false; + // Follow the token chain directly; repeated getToken(i) calls rescan from the start each time + Token next = skipBalancedBracketGroup(innerOpen); if (next == null) return false; return next.kind == K_UNION || next.kind == K_INTERSECT || next.kind == K_EXCEPT || next.kind == K_MINUS; } catch (TokenMgrException e) { return false; } } + /** + * Whether the TABLE at {@code offset} starts DuckDB's TABLE t query rather than naming a + * column, as in (table = 'x'): a table name follows, and then the end of the query. + */ + private boolean isTableQueryAhead(int offset) { + Token token = getToken(offset); + do { + token = nextWithItemToken(token); + if (!isIdentifierishNameToken(token)) { + return false; + } + token = nextWithItemToken(token); + } while (".".equals(token.image)); + switch (token.kind) { + case CLOSING_BRACKET: case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS: + case K_ORDER: case K_LIMIT: case K_OFFSET: case K_FETCH: + return true; + default: + return false; + } + } + protected boolean isParenthesedSelectAhead() { try { if (getToken(1).kind != OPENING_BRACKET) return false; int k = getToken(2).kind; - return k == K_SELECT || k == K_WITH || k == K_VALUES; + // DuckDB's (FROM t ...) and (TABLE t ...) + return k == K_SELECT || k == K_WITH || k == K_VALUES || k == K_FROM + || k == K_TABLE && isTableQueryAhead(2); + } catch (TokenMgrException e) { + return false; + } + } + + /** The literal tokens that may complete a typed literal in {@link #isImplicitCastAhead()}. */ + private boolean isTypedLiteralFollower(int kind) { + return kind == S_CHAR_LITERAL || kind == S_LONG || kind == S_DOUBLE; + } + + /** + * Whether the token could start a {@link #RelObjectName()}, mirroring the token + * alternatives accepted there (base identifiers, DATA_TYPE, non-reserved keywords + * and the reserved-keyword-as-identifier list). + */ + private static boolean isIdentifierishNameToken(Token t) { + switch (t.kind) { + case S_IDENTIFIER: case S_QUOTED_IDENTIFIER: case DATA_TYPE: + case K_DATETIMELITERAL: case K_DATE_LITERAL: + case K_ALL: case K_ANY: case K_BETWEEN: case K_CASEWHEN: case K_CONNECT: case K_CREATE: + case K_DEFAULT: case K_FULL: case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF: + case K_IIF: case K_IGNORE: case K_IN: case K_INTERVAL: case K_LEFT: + case K_LIMIT: case K_NEXTVAL: case K_OFFSET: case K_ON: case K_ONLY: case K_OPTIMIZE: + case K_ORDER: case K_PROCEDURE: case K_PUBLIC: case K_QUALIFY: case K_RETURNING: + case K_RIGHT: case K_SET: case K_SOME: case K_START: case K_TABLES: + case K_TOP: case K_VALUE: case K_VALUES: + return true; + default: + return t.kind >= MIN_NON_RESERVED_WORD && t.kind <= MAX_NON_RESERVED_WORD; + } + } + + private Token nextWithItemToken(Token current) { + if (current.next == null) { + current.next = token_source.getNextToken(); + } + return current.next; + } + + private Token skipBalancedBracketGroup(Token current) { + int depth = 0; + while (!interrupted && current.kind != EOF) { + if (current.kind == OPENING_BRACKET) { + depth++; + } else if (current.kind == CLOSING_BRACKET) { + if (--depth == 0) { + return nextWithItemToken(current); + } + } + current = nextWithItemToken(current); + } + if (interrupted) { + throw new CancellationException("Parsing interrupted"); + } + return null; + } + + private boolean isWithExpressionAliasAhead() { + try { + Token t1 = getToken(1); + if (t1.kind == K_FUNCTION || t1.kind == K_RECURSIVE) { + return false; + } + if (!isIdentifierishNameToken(t1)) { + return true; + } + Token next = getToken(2); + if (next.kind == OPENING_BRACKET) { + next = skipBalancedBracketGroup(next); + if (next == null) { + return false; + } + } + if (next.kind != K_AS) { + return true; + } + next = nextWithItemToken(next); + if (next.kind == K_NOT) { + next = nextWithItemToken(next); + } + if (next.kind == K_MATERIALIZED) { + next = nextWithItemToken(next); + } + return next.kind != OPENING_BRACKET; } catch (TokenMgrException e) { return false; } @@ -481,18 +1359,33 @@ public class CCJSqlParser extends AbstractJSqlParser { if (k1 == K_JSON) return getToken(2).kind == S_CHAR_LITERAL; if (k1 != DATA_TYPE) return false; int k2 = getToken(2).kind; - if (k2 != OPENING_BRACKET) return true; // DATA_TYPE literal - simple cast - // DATA_TYPE( ... ) - precision cast if content is only S_LONG literals - // function call otherwise (e.g. UUID(), VARCHAR(col)) + if (k2 != OPENING_BRACKET) { + // a bare DATA_TYPE is a typed literal (INT '5') only when a trailing + // literal completes it, possibly after further type-name tokens + // (DOUBLE PRECISION '1'); otherwise it is an identifier, e.g. a + // column named number or a keyword-named lambda alias + if (isTypedLiteralFollower(k2)) { + return true; + } + return k2 == DATA_TYPE && isTypedLiteralFollower(getToken(3).kind); + } + // DATA_TYPE( ... ) - precision cast only when a trailing literal completes + // the typed literal (DATA_TYPE(N) 'lit'); a function call otherwise, + // e.g. double(5), int(5), VARCHAR(col), UUID() int k3 = getToken(3).kind; if (k3 == CLOSING_BRACKET) return false; // DATA_TYPE() - empty call if (k3 != S_LONG) return false; // DATA_TYPE(expr) - function call int k4 = getToken(4).kind; - if (k4 == CLOSING_BRACKET) return true; // DATA_TYPE(N) - precision cast - if (k4 != K_COMMA) return false; // DATA_TYPE(N expr) - function call - int k5 = getToken(5).kind; - if (k5 != S_LONG) return false; // DATA_TYPE(N, expr) - function call - return getToken(6).kind == CLOSING_BRACKET; // DATA_TYPE(N,M) - precision cast + if (k4 == K_COMMA) { + int k5 = getToken(5).kind; + if (k5 != S_LONG) return false; // DATA_TYPE(N, expr) - function call + if (getToken(6).kind != CLOSING_BRACKET) { + return false; // DATA_TYPE(N,M expr) - function call + } + return isTypedLiteralFollower(getToken(7).kind); // DATA_TYPE(N,M) 'lit' + } + if (k4 != CLOSING_BRACKET) return false; // DATA_TYPE(N expr) - function call + return isTypedLiteralFollower(getToken(5).kind); // DATA_TYPE(N) 'lit' } catch (TokenMgrException e) { return false; } @@ -530,6 +1423,109 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * XML constructor functions take a bare leading keyword before their first argument, + * e.g. XMLPARSE(CONTENT expr), XMLPARSE(DOCUMENT expr), XMLELEMENT(NAME ident, ...). + * Guarded by the function name so that CONTENT/DOCUMENT/NAME stay plain identifiers + * everywhere else, e.g. f(content). + */ + private void consumeXmlLeadingKeyword(Function function) { + if (!hasUnqualifiedName(function, "XMLPARSE") && !hasUnqualifiedName(function, "XMLELEMENT") + && !hasUnqualifiedName(function, "XMLFOREST")) { + return; + } + Token token = getToken(1); + if (token.kind != S_IDENTIFIER && token.kind != K_NAME) { + return; + } + String keyword = token.image.toUpperCase(java.util.Locale.ROOT); + if (!"CONTENT".equals(keyword) && !"DOCUMENT".equals(keyword) && !"NAME".equals(keyword)) { + return; + } + // only a bare leading keyword: an expression must follow + Token next = getToken(2); + if (next.kind == EOF || next.kind == K_AS || ")".equals(next.image) || ",".equals(next.image) + || ".".equals(next.image) || "(".equals(next.image)) { + return; + } + function.setExtraKeyword(getNextToken().image); + } + + // Matches the single name part, since getName() joins the parts into a new string on each call. + private static boolean hasUnqualifiedName(Function function, String name) { + List nameParts = function.getMultipartName(); + return nameParts != null && nameParts.size() == 1 && name.equalsIgnoreCase(nameParts.get(0)); + } + + /** + * An unquoted YEAR, MONTH, DAY, HOUR, MINUTE or SECOND passed on its own as a function argument, as in + * DATEADD(day, 1, d), is a date unit. Anywhere else it is an ordinary identifier, so it is parsed as a + * Column and only converted here, once it is known to be a whole argument. + */ + private void normalizeDateUnitArguments(Function function) { + ExpressionList parameters = function.getParameters(); + if (parameters == null || parameters instanceof ParenthesedExpressionList) { + return; + } + ExpressionList normalized = null; + for (int i = 0; i < parameters.size(); i++) { + if (!isDateUnitColumn(parameters.get(i))) { + continue; + } + Column column = (Column) parameters.get(i); + DateUnitExpression unit = new DateUnitExpression(column.getColumnName()); + linkAST(unit, column.getASTNode()); + if (normalized == null) { + normalized = new ExpressionList(parameters); + normalized.setASTNode(parameters.getASTNode()); + } + normalized.set(i, unit); + } + if (normalized != null) { + function.setParameters(normalized); + } + } + + private static boolean isDateUnitColumn(Expression expression) { + if (!(expression instanceof Column)) { + return false; + } + Node node = ((Column) expression).getASTNode(); + if (node == null) { + return false; + } + Token first = node.jjtGetFirstToken(); + return first != null && first == node.jjtGetLastToken() && first.kind == K_DATE_LITERAL; + } + + private void normalizeOdbcTimestampInterval(Function function) { + if (!function.isEscaped() || function.getMultipartName().size() != 1 + || !("TIMESTAMPADD".equalsIgnoreCase(function.getName()) + || "TIMESTAMPDIFF".equalsIgnoreCase(function.getName()))) { + return; + } + ExpressionList parameters = function.getParameters(); + if (parameters == null || parameters.size() != 3 + || parameters instanceof ParenthesedExpressionList + || !(parameters.get(0) instanceof Column)) { + return; + } + Column column = (Column) parameters.get(0); + if (column.getTable() != null) { + return; + } + DateUnitExpression.DateUnit unit = + DateUnitExpression.DateUnit.fromOdbcInterval(column.getColumnName()); + if (unit != null) { + DateUnitExpression interval = new DateUnitExpression(unit); + linkAST(interval, column.getASTNode()); + ExpressionList normalized = new ExpressionList(parameters); + normalized.setASTNode(parameters.getASTNode()); + normalized.set(0, interval); + function.setParameters(normalized); + } + } + private boolean isKeywordArgumentAhead() { Token t = getToken(1); if (t.kind == EOF || t.image.equals(")")) return false; @@ -565,26 +1561,54 @@ public class CCJSqlParser extends AbstractJSqlParser { /** * Scans ahead through a dotted identifier chain and checks if '*' follows. * Identifies table.* patterns for AllTableColumns. + * Long chains are memoized per start token like {@link #isFunctionAhead()} + * (one walk fills every eligible chain part); short chains skip the cache. */ + private final Map isAllTableColumnsAheadCache = + new HashMap(); + protected boolean isAllTableColumnsAhead() { - int i = 1; - Token t = getToken(i); + int r = isAllTableColumnsWalk(false); + if (r != LONG_CHAIN) { + return r == 1; + } + Token key = getToken(1); + Boolean cached = isAllTableColumnsAheadCache.get(key); + if (cached != null) { + return cached; + } + boolean result = isAllTableColumnsWalk(true) == 1; + isAllTableColumnsAheadCache.put(key, result); + return result; + } + + // Returns 1 for true, 0 for false, or LONG_CHAIN when the walk exceeded + // CHAIN_CACHE_THRESHOLD delimiter/part pairs (only in the non-filling pass). + private int isAllTableColumnsWalk(boolean fill) { + Token last = getToken(1); // Must start with a name-like token - if (t.kind == S_LONG || t.kind == S_DOUBLE || t.kind == S_HEX - || t.kind == S_CHAR_LITERAL || t.kind == OPENING_BRACKET - || t.kind == CLOSING_BRACKET || t.kind == EOF) { - return false; + if (last.kind == S_LONG || last.kind == S_DOUBLE || last.kind == S_HEX + || last.kind == S_CHAR_LITERAL || last.kind == OPENING_BRACKET + || last.kind == CLOSING_BRACKET || last.kind == EOF) { + return 0; } - i++; - // Walk through dotted name chain + // Walk through dotted name chain, following the token chain directly; + // repeated getToken(i) calls rescan from the start each time + int delimiters = 0; + List chainParts = fill ? new ArrayList() : null; while (true) { - t = getToken(i); + Token t = nextWithItemToken(last); if (t.image.equals(".") || t.image.equals("..") || t.image.equals("...")) { - i++; // skip delimiter - i++; // skip next part (could be "*") + if (!fill && ++delimiters > CHAIN_CACHE_THRESHOLD) { + return LONG_CHAIN; + } + last = nextWithItemToken(t); // skip delimiter, take next part (could be "*") + if (fill) { + chainParts.add(last); + } } else { break; } @@ -592,8 +1616,145 @@ public class CCJSqlParser extends AbstractJSqlParser { // It's AllTableColumns if the chain ended on "*" // i.e., the last name part we skipped over was "*" - // Back up: the last token consumed was at (i-1) - return getToken(i - 1).image.equals("*"); + boolean result = last.image.equals("*"); + + // Each chain part that would pass the name-like guard walks the same + // remaining suffix and obtains the same answer. + if (fill) { + for (Token part : chainParts) { + if (part.kind == S_LONG || part.kind == S_DOUBLE || part.kind == S_HEX + || part.kind == S_CHAR_LITERAL || part.kind == OPENING_BRACKET + || part.kind == CLOSING_BRACKET || part.kind == EOF) { + continue; + } + isAllTableColumnsAheadCache.put(part, result); + } + } + return result ? 1 : 0; + } + + /** + * Detects PostgreSQL composite row expansion {@code (function()).*} (and any + * number of surrounding parentheses), where a parenthesised expression wrapping + * a single {@link Function} is immediately followed by {@code .*}. + * + *

This is a constant-time follower check (unwraps the already-parsed + * {@code retval} and peeks the next two tokens). It deliberately avoids the + * speculative syntactic lookahead that previously degraded performance, see + * issue #2207. + * + * @param retval the expression parsed so far within the + * {@code ParenthesedExpressionList} branch of {@code PrimaryExpression} + */ + protected boolean isFunctionAllColumnsAhead(Expression retval) { + // Fast follower gate: the overwhelming majority of parenthesised + // expressions are not followed by ".*", so reject on the token stream + // before ever touching the already-parsed expression. + if (!getToken(1).image.equals(".") || !getToken(2).image.equals("*")) { + return false; + } + if (retval == null) { + return false; + } + + Expression inner = retval; + while (inner instanceof ParenthesedExpressionList) { + ParenthesedExpressionList parenthesed = (ParenthesedExpressionList) inner; + if (parenthesed.size() != 1) { + return false; + } + inner = parenthesed.get(0); + } + + return inner instanceof Function; + } + + /** + * Unwraps any number of surrounding parentheses from a parenthesised + * {@link Function} and returns the inner function. Only call this after + * {@link #isFunctionAllColumnsAhead(Expression)} has confirmed the shape. + */ + protected Function unwrapParenthesedFunction(Expression retval) { + Expression inner = retval; + while (inner instanceof ParenthesedExpressionList) { + inner = ((ParenthesedExpressionList) inner).get(0); + } + return (Function) inner; + } + + /** + * Whether the next token can continue the (possibly multi-word) keyword of + * an OPTION query hint, e.g. any word of {@code HASH JOIN}, {@code KEEP PLAN}, + * {@code OPTIMIZE FOR UNKNOWN} or {@code IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX}. + * + * Reserved words usable inside hint keywords are enumerated here because + * they cannot be matched by {@code RelObjectName()}. + */ + private boolean isOptionHintWordAhead() { + int kind = getToken(1).kind; + if (kind == S_IDENTIFIER + || (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD)) { + return true; + } + switch (kind) { + case K_JOIN: case K_FOR: case K_ORDER: + case K_UNION: case K_EXCEPT: case K_INTERSECT: + case K_USE: case K_OPTIMIZE: + return true; + default: + return false; + } + } + + /** + * Whether the next token can start the single value argument of an OPTION + * query hint, e.g. the {@code 100} of {@code FAST 100} or the {@code 25} + * of {@code MAX_GRANT_PERCENT = 25}. + */ + private boolean isOptionHintValueAhead() { + switch (getToken(1).kind) { + case S_LONG: case S_DOUBLE: case S_CHAR_LITERAL: + case S_PARAMETER: + return true; + default: + return false; + } + } + + /** + * Both named-parameter forms match only a single-token name (or K_OUTER) directly followed + * by their assignment token; the 2-token lookahead fails for anything else, so this first + * check is equivalent to running that scan. + */ + private boolean isNamedParameterAssignmentAhead() { + int kind = getToken(2).kind; + return kind == K_ORACLE_NAMED_PARAMETER_ASSIGNMENT + || kind == K_POSTGRES_NAMED_PARAMETER_ASSIGNMENT; + } + + /** Distinguish EXISTS identifiers from predicates before suffixes and clause boundaries. */ + private boolean isExistsIdentifierAhead() { + if (getToken(1).kind != K_EXISTS) { + return false; + } + if ((ternaryThenBranchDepth > 0 || mapKeyDepth > 0) + && ":".equals(getToken(2).image)) { + return true; + } + int kind = getToken(2).kind; + if (kind == OPENING_BRACKET) { + return "+".equals(getToken(3).image) && getToken(4).kind == CLOSING_BRACKET; + } + if (kind == K_LIMIT || kind == K_OFFSET) { + Token value = getToken(3); + return value.kind == S_LONG || value.kind == S_DOUBLE || value.kind == S_PARAMETER + || value.kind == K_ALL || value.kind == K_NULL + || "?".equals(value.image) || ":".equals(value.image) + || "&".equals(value.image); + } + return isConditionKeywordAhead(2) + || kind == K_ORDER && (getToken(3).kind == K_BY || getToken(3).kind == K_SIBLINGS) + || kind == K_GROUP && getToken(3).kind == K_BY; } /** @@ -621,19 +1782,28 @@ public class CCJSqlParser extends AbstractJSqlParser { switch (kind) { // Safe as implicit identifiers (no clause collision) - case K_TABLES: case K_OPTIMIZE: case K_PROCEDURE: case K_PUBLIC: + case K_TABLES: case K_OPTIMIZE: case K_PUBLIC: case K_CASEWHEN: case K_IIF: return true; // Safe when the follower doesn't form a clause + case K_PROCEDURE: return nextKind != K_ANALYSE; case K_GROUP: return nextKind != K_BY; case K_ORDER: return nextKind != K_BY && nextKind != K_SIBLINGS; case K_CONNECT: return nextKind != K_BY; case K_START: return nextKind != K_WITH; case K_LEFT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI; + && nextKind != K_SEMI && nextKind != K_ANTI + && nextKind != K_ARRAY_LITERAL; case K_RIGHT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI; + && nextKind != K_SEMI && nextKind != K_ANTI + && nextKind != K_ARRAY_LITERAL; + case K_FULL: return nextKind != K_JOIN && nextKind != K_OUTER + && nextKind != K_SEMI && nextKind != K_ANTI + && nextKind != K_ANY && nextKind != K_ALL + && nextKind != K_ARRAY_LITERAL; + case K_ARRAY_LITERAL: + return nextKind != K_JOIN; case K_ALL: return nextKind != K_JOIN; case K_ANY: return nextKind != OPENING_BRACKET; case K_SOME: return nextKind != OPENING_BRACKET; @@ -697,8 +1867,12 @@ public class CCJSqlParser extends AbstractJSqlParser { return true; } + if (getToken(1).kind == K_ONLY && isRelationOnlyAhead(prev)) { + return false; + } + // ── 2. After delimiters and operators: expression position ───── - // With K_FROM/K_SELECT/K_CURRENT removed from RelObjectName's + // With K_FROM/K_SELECT removed from RelObjectName's // token list, accepting all remaining keywords after these // delimiters and operators is safe. if (!prev.image.isEmpty()) { @@ -759,6 +1933,10 @@ public class CCJSqlParser extends AbstractJSqlParser { // Set operators case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS: + // TABLE and COLUMN are always followed by the object name, as in + // CREATE TABLE value (...) or ADD COLUMN offset INT + case K_TABLE: case K_COLUMN: + // DDL / utility keywords (never identifiers) case K_FOREIGN: case K_CONSTRAINT: case K_UNIQUE: case K_CHECK: case K_FORCE: @@ -776,325 +1954,2077 @@ public class CCJSqlParser extends AbstractJSqlParser { } /** - * Checks whether the next token(s) can plausibly start an {@code Alias}. - * - * Used as a semantic LOOKAHEAD guard at alias call-sites. Unlike - * {@link #isReservedKeywordAsIdentifier()}, this method does NOT use - * the structural-keyword whitelist (step 4), because an alias always - * follows an expression — never a structural keyword in isolation. - * Instead it goes directly to follower-based disambiguation for - * reserved keywords. + * Whether ONLY is PostgreSQL's prefix of a relation name, as in DELETE FROM ONLY t, + * JOIN ONLY t or FROM a, ONLY b, rather than a table or column named only. */ - private boolean isAliasAhead() { - Token t = getToken(1); - int kind = t.kind; - - // AS always starts an alias - if (kind == K_AS) return true; - - // String-literal alias: SELECT col 'myAlias' - if (kind == S_CHAR_LITERAL) return true; - - // Base identifier tokens - if (kind == S_IDENTIFIER || kind == S_QUOTED_IDENTIFIER - || kind == DATA_TYPE || kind == K_DATETIMELITERAL - || kind == K_DATE_LITERAL) { - return true; - } - - // Non-reserved keywords - if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) { - return true; - } - - // For reserved keywords in alias position, skip the structural- - // keyword whitelist and go directly to follower disambiguation. - // Only check keywords that are actually in RelObjectName's token - // alternatives — don't fire for brackets, operators, literals, etc. - switch (kind) { - case K_ALL: case K_ANY: case K_CASEWHEN: case K_CONNECT: - case K_CREATE: case K_DEFAULT: - case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF: - case K_IIF: case K_IGNORE: case K_IN: case K_INTERVAL: - case K_LEFT: case K_LIMIT: case K_NEXTVAL: case K_OFFSET: - case K_ON: case K_OPTIMIZE: case K_ORDER: case K_PROCEDURE: - case K_PUBLIC: case K_QUALIFY: case K_RIGHT: - case K_SET: case K_SOME: case K_START: case K_TABLES: - case K_TOP: case K_VALUE: case K_VALUES: - return isReservedKeywordSafeByFollower(); - default: - return false; - } + private boolean isRelationOnlyAhead(Token prev) { + return RelationOptions.isOnlyAhead(prev, getToken(2).kind); } /** - * Lightweight lookahead for SpecialStringFunctionWithNamedParameters: - * scans forward from the current position (just past the opening '(') - * looking for FROM, IN, or PLACING at bracket nesting depth 0. - * - * This replaces an expensive syntactic LOOKAHEAD(NamedExpressionListExprFirst()) - * that caused exponential backtracking with deeply nested expressions. + * DuckDB's FROM-first query: a FROM clause followed by query clauses the pipe syntax cannot + * consume. Scanning stops at a pipe operator or the end of the statement, and clause keywords + * nested in brackets (a sub-select, for instance) do not count. */ - protected boolean isNamedExprListAhead() { + private boolean isFromFirstSelectAhead() { + Token token = getToken(1); + if (token.kind != K_FROM) { + return false; + } int depth = 0; - for (int i = 1; ; i++) { - Token t = getToken(i); - if (t.kind == EOF) { + while (true) { + token = nextWithItemToken(token); + if (token.kind == EOF || ";".equals(token.image) || "|>".equals(token.image)) { return false; } - if (t.kind == OPENING_BRACKET) { + if ("(".equals(token.image)) { depth++; - } else if (t.kind == CLOSING_BRACKET) { - if (depth == 0) { + } else if (")".equals(token.image)) { + // the end of the parenthesized query this FROM starts + if (--depth < 0) { return false; } - depth--; } else if (depth == 0) { - if (t.kind == K_FROM || t.kind == K_IN || t.kind == K_PLACING) { - return true; - } - if (t.image.equals(",")) { - return false; + switch (token.kind) { + case K_UNION: + case K_EXCEPT: + case K_INTERSECT: + case K_MINUS: + case K_SELECT: + case K_WHERE: + case K_GROUP: + case K_HAVING: + case K_QUALIFY: + case K_ORDER: + case K_LIMIT: + case K_OFFSET: + return true; + default: + break; } } } } + /** CREATE [TEMP|TEMPORARY] MACRO, DuckDB's inlined function definition. */ + private boolean isCreateMacroAhead() { + int offset = 1; + int kind = getToken(offset).kind; + if (kind == K_TEMP || kind == K_TEMPORARY) { + offset++; + } + return getToken(offset).kind == K_MACRO; + } + /** - * Checks if the next token can start a condition suffix - * (comparison, IN, BETWEEN, LIKE, IS NULL, etc.) - * - * Used as the entry guard for the entire optional condition-suffix block - * in Condition(), eliminating choice conflicts. + * DuckDB's FUNCTION alias of MACRO: FUNCTION [IF NOT EXISTS] name (parameters) AS body, where + * AS directly follows the parameters, the body is not a string and no LANGUAGE or OPTIONS + * clause follows it, unlike other dialects' CREATE FUNCTION (RETURNS ..., PostgreSQL's + * AS $tag$ ... $tag$ LANGUAGE ..., BigQuery's AS (expr) OPTIONS (...)). */ - protected boolean isConditionSuffixAhead() { - Token t = getToken(1); - // Inline isComparisonOperatorAhead for single getToken(1) call - if (t.kind == OPENING_BRACKET && getToken(2).image.equals("+")) { - return isComparisonOperator(getToken(4)); + private boolean isCreateFunctionMacroAhead() { + int offset = 1; + int kind = getToken(offset).kind; + if (kind == K_TEMP || kind == K_TEMPORARY) { + offset++; } - if (isComparisonOperator(t)) return true; - switch (t.kind) { - // Each suffix's start token: - case K_OVERLAPS: // OVERLAPS - case K_IN: // IN - case K_GLOBAL: // GLOBAL ... IN - case K_EXCLUDES: // EXCLUDES (...) - case K_INCLUDES: // INCLUDES (...) - case K_BETWEEN: // BETWEEN - case K_MEMBER: // MEMBER OF - case K_IS: // IS [NOT] NULL / TRUE / FALSE / UNKNOWN / DISTINCT - case K_ISNULL: // ISNULL - case K_NOTNULL: // NOTNULL - case K_LIKE: // LIKE - case K_ILIKE: // ILIKE - case K_RLIKE: // RLIKE - case K_REGEXP_LIKE: // REGEXP_LIKE - case K_REGEXP: // REGEXP - case K_SIMILAR_TO: // SIMILAR TO (in LikeExpression) - case K_SIMILAR: // SIMILAR TO (in SimilarToExpression) - case K_MATCH_ANY: // MATCH_ANY - case K_MATCH_ALL: // MATCH_ALL - case K_MATCH_PHRASE: // MATCH_PHRASE - case K_MATCH_PHRASE_PREFIX: // MATCH_PHRASE_PREFIX - case K_MATCH_REGEXP: // MATCH_REGEXP - case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR + if (getToken(offset).kind != K_FUNCTION) { + return false; + } + // IF NOT EXISTS and a qualified name, at most 8 tokens, up to the parameter list + int nameEnd = offset + 8; + offset++; + while (getToken(offset).kind != OPENING_BRACKET) { + kind = getToken(offset).kind; + if (offset > nameEnd || kind == EOF || kind == ST_SEMICOLON) { + return false; + } + offset++; + } + int depth = 0; + do { + kind = getToken(offset).kind; + if (kind == OPENING_BRACKET) { + depth++; + } else if (kind == CLOSING_BRACKET) { + depth--; + } else if (kind == EOF) { + return false; + } + offset++; + } while (depth > 0); + if (getToken(offset).kind != K_AS) { + return false; + } + Token body = getToken(offset + 1); + if (body.kind == S_CHAR_LITERAL || body.kind == S_DOLLAR_QUOTED_STRING + || body.image.length() > 1 && body.image.startsWith("$") && body.image.endsWith("$")) { + return false; + } + // no LANGUAGE or OPTIONS clause outside parentheses up to the end of the statement + depth = 0; + for (offset++;; offset++) { + Token token = getToken(offset); + if (token.kind == EOF || depth == 0 && token.kind == ST_SEMICOLON) { return true; - // Oracle (+) before IN: col(+) IN (...) - case OPENING_BRACKET: - return getToken(2).image.equals("+"); - default: + } + if (token.kind == OPENING_BRACKET) { + depth++; + } else if (token.kind == CLOSING_BRACKET) { + depth--; + } else if (depth == 0 && ("LANGUAGE".equalsIgnoreCase(token.image) + || "OPTIONS".equalsIgnoreCase(token.image))) { return false; + } } } + // the % of DuckDB's LIMIT 10% in the LIMIT clause being parsed, which ends its row count + private Token limitPercentToken = null; -} + /** + * Finds the % that ends the row count of the LIMIT clause starting after the current token: + * at its top level and directly followed by the end of the clause, as in LIMIT 10% OFFSET 2. + */ + private Token findLimitPercentToken() { + int depth = 0; + for (Token token = nextWithItemToken(getToken(0));; token = nextWithItemToken(token)) { + if (token.kind == EOF || token.kind == ST_SEMICOLON) { + return null; + } + if ("(".equals(token.image) || "[".equals(token.image)) { + depth++; + } else if (")".equals(token.image) || "]".equals(token.image)) { + if (--depth < 0) { + return null; + } + } else if (depth == 0 && "%".equals(token.image) + && isLimitClauseEnd(nextWithItemToken(token))) { + return token; + } + } + } -PARSER_END(CCJSqlParser) + /** Whether the token ends a LIMIT clause; OFFSET and FETCH only when they start a clause. */ + private boolean isLimitClauseEnd(Token token) { + switch (token.kind) { + case EOF: case ST_SEMICOLON: case CLOSING_BRACKET: + case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS: + return true; + case K_OFFSET: + // not a column named offset, as in LIMIT a % offset + 1 + Token value = nextWithItemToken(token); + return value.kind == S_LONG || value.kind == OPENING_BRACKET + || "?".equals(value.image) || ":".equals(value.image); + case K_FETCH: + Token next = nextWithItemToken(token); + return next.kind == K_FIRST || next.kind == K_NEXT; + default: + return false; + } + } -TOKEN_MGR_DECLS : { - public FeatureConfiguration configuration = new FeatureConfiguration(); + /** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */ + private boolean isSampleMethod(Token token) { + return token.kind == K_SYSTEM || token.kind == K_BERNOULLI + || (token.image != null && "RESERVOIR".equalsIgnoreCase(token.image)); + } + /** DuckDB's INSERT ... BY NAME|POSITION column matching, never a table alias. */ + private boolean isColumnMatchingAhead() { + return getToken(1).kind == K_BY + && ("NAME".equalsIgnoreCase(getToken(2).image) + || "POSITION".equalsIgnoreCase(getToken(2).image)); + } - // Identify the index of the quoting/escaping tokens - public int charLiteralIndex = -1; - public int squaredBracketOpenIndex = -1; - { - for (int i=0;i") ) { - charLiteralIndex = i; - break; - } - } - for (int i=0;i= 0; i--) { - if (s.charAt(i) == '\\' && s.charAt(i + 1) == '\'' && s.charAt(i + 2) == '\'') { - return i; + /** ASOF stays a contextual identifier outside a join prefix. */ + private boolean isAsOfJoinAhead() { + if (!isKeywordAhead("ASOF")) { + return false; + } + int offset = 2; + int kind = getToken(offset).kind; + if (kind == K_LEFT || kind == K_RIGHT || kind == K_FULL) { + offset++; + if (getToken(offset).kind == K_OUTER) { + offset++; } + } else if (kind == K_INNER) { + offset++; } - return -1; + return getToken(offset).kind == K_JOIN; } - public void CommonTokenAction(Token t) - { - t.absoluteBegin = getCurrentTokenAbsolutePosition(); - t.absoluteEnd = t.absoluteBegin + t.image.length(); - } + /** + * Checks whether the next token(s) can plausibly start an {@code Alias}. + * + * Used as a semantic LOOKAHEAD guard at alias call-sites. Unlike + * {@link #isReservedKeywordAsIdentifier()}, this method does NOT use + * the structural-keyword whitelist (step 4), because an alias always + * follows an expression — never a structural keyword in isolation. + * Instead it goes directly to follower-based disambiguation for + * reserved keywords. + */ + private boolean isMatchRecognizeAhead() { + return isKeywordAhead("MATCH_RECOGNIZE") && getToken(2).kind == OPENING_BRACKET; + } - public int getCurrentTokenAbsolutePosition() - { - if (input_stream instanceof SimpleCharStream) - return ((SimpleCharStream)input_stream).getAbsoluteTokenBegin(); - return -1; - } + private boolean isCreateTableQueryPrefix() { + int kind = getToken(1).kind; + return kind == K_AS || kind == K_TABLE || kind == K_IGNORE || kind == K_REPLACE; + } - private static boolean endsWithDelimiter(Deque windowQueue, String delimiter) { - if (windowQueue.size() != delimiter.length()) { + private boolean isRowPatternPrimaryAhead() { + Token next = getToken(1); + return next.kind == OPENING_BRACKET || next.kind == OP_CARET + || next.kind == OPENING_CURLY_BRACKET && "-".equals(getToken(2).image) + || next.kind == S_IDENTIFIER || next.kind == S_QUOTED_IDENTIFIER + || next.kind == DATA_TYPE + || next.kind >= MIN_NON_RESERVED_WORD && next.kind <= MAX_NON_RESERVED_WORD; + } + + private static boolean isSliceDelimiter(Token token) { + return ":".equals(token.image) || "]".equals(token.image) || ",".equals(token.image); + } + + /** Whether a {@code [} opens DuckDB's {@code [expr FOR x IN list]}: FOR at its top level. */ + private boolean isListComprehensionAhead() { + Token token = getToken(1); + if (!"[".equals(token.image)) { return false; } - - int i = 0; - for (char ch : windowQueue) { - if (ch != delimiter.charAt(i++)) { + int depth = 0; + for (;; token = nextWithItemToken(token)) { + if (token.kind == EOF || ";".equals(token.image)) { return false; } + if ("(".equals(token.image) || "[".equals(token.image) || "{".equals(token.image)) { + depth++; + } else if (")".equals(token.image) || "]".equals(token.image) + || "}".equals(token.image)) { + if (--depth == 0) { + return false; + } + } else if (depth == 1 && token.kind == K_FOR) { + return true; + } } - return true; } - public void consumeDollarQuotedString(String closingQuote) { - Deque windowQueue = new ArrayDeque(); - int delimiterLength = closingQuote.length(); + /** + * Whether the pending "," ends a select item list, as DuckDB allows: it is followed by a + * closing parenthesis or by a clause that cannot be read as a column such as limit or "group". + */ + private boolean isTrailingSelectItemCommaAhead() { + if (!",".equals(getToken(1).image)) { + return false; + } + Token next = getToken(2); + int following = getToken(3).kind; + switch (next.kind) { + case CLOSING_BRACKET: + case K_FROM: case K_WHERE: case K_HAVING: case K_WINDOW: case K_INTO: + case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS: + return true; + case K_GROUP: case K_ORDER: + // not a column named group aliased by: SELECT a, group by FROM t + int afterBy = getToken(4).kind; + return following == K_BY && afterBy != K_FROM && afterBy != EOF + && afterBy != CLOSING_BRACKET && afterBy != ST_SEMICOLON + && !",".equals(getToken(4).image); + case K_LIMIT: case K_OFFSET: + return following == S_LONG || "?".equals(getToken(3).image); + default: + return false; + } + } - try { - while (true) { - char ch = input_stream.readChar(); - windowQueue.addLast(ch); - if (windowQueue.size() > delimiterLength) { - windowQueue.removeFirst(); + /** + * Whether the REPLACE at {@code offset} is DuckDB's unparenthesized {@code REPLACE expr AS name}: + * an AS follows at the top level before the end of the select item, so that a star aliased + * "replace" keeps parsing. With {@code offset} 0, the REPLACE has been consumed. + */ + private boolean isUnparenthesizedReplaceAhead(int offset) { + Token token = getToken(offset); + if (offset > 0 && token.kind != K_REPLACE) { + return false; + } + int depth = 0; + while (true) { + token = nextWithItemToken(token); + if (token.kind == EOF || ";".equals(token.image)) { + return false; + } + if ("(".equals(token.image) || "[".equals(token.image) || "{".equals(token.image)) { + depth++; + } else if (")".equals(token.image) || "]".equals(token.image) + || "}".equals(token.image)) { + if (--depth < 0) { + return false; } - if (endsWithDelimiter(windowQueue, closingQuote)) { - return; + } else if (depth == 0) { + if (token.kind == K_AS) { + return true; + } + if (token.kind == K_FROM || ",".equals(token.image)) { + return false; } } - } catch (java.io.IOException e) { - reportError(Math.max(closingQuote.length(), input_stream.GetImage().length())); } } + private boolean isUnparenthesizedReplaceAhead() { + return isUnparenthesizedReplaceAhead(1); + } + /** - * Consumes the body of a block comment after the opening delimiter has been matched, - * honouring nesting, up to and including the outermost closing delimiter. Then backs - * up 2 characters so the MULTI_LINE_COMMENT rule matches the closing delimiter and - * terminates the MORE chain. + * Whether DuckDB's {@code RENAME (column AS name, ...)} or {@code RENAME column AS name} + * follows, every column being renamed with AS, so that a star aliased "rename" keeps parsing. */ - void consumeBlockCommentBody() { - int nesting = 1; - char prev = 0; - try { - while (nesting > 0) { - char c = input_stream.readChar(); - if (prev == '/' && c == '*') { - nesting++; - prev = 0; - } else if (prev == '*' && c == '/') { - nesting--; - prev = 0; - } else { - prev = c; + private boolean isRenameAhead() { + Token token = getToken(1); + if (token.kind != K_RENAME) { + return false; + } + token = nextWithItemToken(token); + boolean parenthesized = "(".equals(token.image); + if (parenthesized) { + token = nextWithItemToken(token); + } + while (true) { + // column [. column]* AS name + while (true) { + if (!isIdentifierishNameToken(token)) { + return false; } + token = nextWithItemToken(token); + if (!".".equals(token.image)) { + break; + } + token = nextWithItemToken(token); } - input_stream.backup(2); - } catch (java.io.IOException e) { - // Unterminated comment: leave the stream at EOF and let the token - // manager raise the lexical error from IN_BLOCK_COMMENT. + if (token.kind != K_AS) { + return false; + } + token = nextWithItemToken(token); + if (!isIdentifierishNameToken(token)) { + return false; + } + token = nextWithItemToken(token); + if (!parenthesized) { + return true; + } + if (")".equals(token.image)) { + return true; + } + if (!",".equals(token.image)) { + return false; + } + token = nextWithItemToken(token); } } -} -SKIP: -{ - -} + /** DuckDB's unparenthesized REPLACE and RENAME name their single item: * RENAME i AS x. */ + private static void requireTransformerAlias(SelectItem item) throws ParseException { + if (item.getAlias() == null) { + throw new ParseException("Expected AS and a name after " + item); + } + } -// Sentinel: start of non-reserved keyword range (for O(1) identifier checks) -SKIP: -{ - -} + /** Whether a window specification starts with the name of the window it extends. */ + private boolean isBaseWindowNameAhead() { + Token token = getToken(1); + switch (token.kind) { + case K_PARTITION: case K_ORDER: case K_ROWS: case K_RANGE: case K_GROUPS: + return false; + default: + return isIdentifierishNameToken(token); + } + } -/** - * Non-reserved SQL keywords usable as unquoted identifiers. - * Tokens are declared inline to get consecutive kind values between - * MIN_NON_RESERVED_WORD and MAX_NON_RESERVED_WORD sentinels. - */ -String NonReservedWord() : -{ Token tk = null; } -{ - ( - tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= + private boolean isBulkCollectIntoAhead() { + return "BULK".equalsIgnoreCase(getToken(1).image) + && "COLLECT".equalsIgnoreCase(getToken(2).image) + && getToken(3).kind == K_INTO; + } + + private boolean isAliasAhead() { + if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead() + || isBulkCollectIntoAhead()) { + return false; + } + Token t = getToken(1); + int kind = t.kind; + + // AS always starts an alias + if (kind == K_AS) return true; + + // SAMPLE followed by a size or BLOCK starts a sampling clause. + if (kind == K_SAMPLE && (getToken(2).kind == OPENING_BRACKET + || getToken(2).kind == K_BLOCK || getToken(2).kind == S_LONG + || getToken(2).kind == S_DOUBLE)) { + return false; + } + + // String-literal alias: SELECT col 'myAlias' + if (kind == S_CHAR_LITERAL) return true; + + // WITHIN followed by a duration starts a ksqlDB JOIN window. + if (kind == K_WITHIN && (getToken(2).kind == S_LONG + || getToken(2).kind == OPENING_BRACKET && getToken(3).kind == S_LONG)) { + return false; + } + + // OPTION (...) introduces a query hint clause, not an alias + if (kind == K_OPTION && getToken(2).kind == OPENING_BRACKET) { + return false; + } + + // ClickHouse's PREWHERE and SETTINGS clauses, not aliases + if (kind == K_PREWHERE || kind == K_SETTINGS) { + return false; + } + + // MySQL's FORCE INDEX|KEY (...) index hint, not an alias + if (kind == K_FORCE && (getToken(2).kind == K_INDEX || getToken(2).kind == K_KEY)) { + return false; + } + + // GRANT after a CREATE SCHEMA element starts the next element + if (kind == K_GRANT && schemaElementDepth > 0) { + return false; + } + + // For reserved keywords in alias position, skip the structural- + // keyword whitelist and go directly to follower disambiguation. + // Only check keywords that are actually in RelObjectName's token + // alternatives — don't fire for brackets, operators, literals, etc. + switch (kind) { + case K_ALL: case K_ARRAY_LITERAL: case K_ANY: case K_CASEWHEN: case K_CONNECT: + case K_CREATE: case K_DEFAULT: case K_FULL: + case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF: + case K_IIF: case K_IGNORE: case K_IN: case K_INTERVAL: + case K_LEFT: case K_LIMIT: case K_NEXTVAL: case K_OFFSET: + case K_ON: case K_OPTIMIZE: case K_ORDER: case K_PROCEDURE: + case K_PUBLIC: case K_QUALIFY: case K_RIGHT: + case K_SET: case K_SOME: case K_START: case K_TABLES: + case K_TOP: case K_VALUE: case K_VALUES: + return isReservedKeywordSafeByFollower(); + default: + break; + } + + // Base identifier tokens + if (kind == S_IDENTIFIER || kind == S_QUOTED_IDENTIFIER + || kind == DATA_TYPE || kind == K_DATETIMELITERAL + || kind == K_DATE_LITERAL) { + return true; + } + + // Non-reserved keywords + if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) { + return true; + } + + return false; + } + + private static void requireTypeDdlSyntax(boolean valid, String message) throws ParseException { + if (!valid) { throw new ParseException(message); } + } + + private static > E typeDdlEnum(Class type, String value) throws ParseException { + try { return Enum.valueOf(type, value.toUpperCase(Locale.ROOT)); } + catch (IllegalArgumentException exception) { throw new ParseException("Unknown " + type.getSimpleName() + ": " + value); } + } + + private static void requireReplicationSyntax(boolean valid, String message) throws ParseException { + if (!valid) { throw new ParseException(message); } + } + + private static void requireDeleteSyntax(boolean valid, String message) throws ParseException { + if (!valid) { throw new ParseException(message); } + } + + private static > E replicationEnum(Class type, String value) throws ParseException { + try { return Enum.valueOf(type, MultiPartName.unquote(value).toUpperCase(Locale.ROOT)); } + catch (IllegalArgumentException exception) { throw new ParseException("Unknown " + type.getSimpleName() + ": " + value); } + } + + /** + * DuckDB's lambda x, y : expr. LAMBDA stays a plain identifier, so a column, type or alias + * named lambda keeps parsing: only LAMBDA followed by names separated by commas and a ":" is + * a lambda. + */ + private boolean isDuckDBLambdaAhead() { + if (getToken(1).kind != S_IDENTIFIER || !isKeywordAhead("LAMBDA")) { + return false; + } + int offset = 2; + while (true) { + int kind = getToken(offset).kind; + if (kind != S_IDENTIFIER && kind != S_QUOTED_IDENTIFIER && kind != DATA_TYPE + && (kind < MIN_NON_RESERVED_WORD || kind > MAX_NON_RESERVED_WORD)) { + return false; + } + String next = getToken(offset + 1).image; + if (":".equals(next)) { + return true; + } + if (!",".equals(next)) { + return false; + } + offset += 2; + } + } + + private boolean isKeywordAhead(String keyword) { + Token token = getToken(1); + return token.kind != EOF && token.image != null && keyword.equalsIgnoreCase(token.image); + } + + /** + * Whether a MySQL CHECK, CHECKSUM, OPTIMIZE or REPAIR TABLE statement starts here. TABLE or + * TABLES must follow, so other uses of these words are unaffected. + */ + private boolean isTableMaintenanceAhead() { + int kind = getToken(1).kind; + if (kind != K_CHECK && kind != K_CHECKSUM && kind != K_OPTIMIZE && kind != K_REPAIR) { + return false; + } + int offset = 2; + int modifier = getToken(2).kind; + if ((kind == K_OPTIMIZE || kind == K_REPAIR) + && (modifier == K_NO_WRITE_TO_BINLOG || modifier == K_LOCAL)) { + offset = 3; + } + int table = getToken(offset).kind; + return table == K_TABLE || table == K_TABLES; + } + + /** + * Whether NO_WRITE_TO_BINLOG or LOCAL modifies a MySQL partition operation. Followed by a + * comma, INTO or the end of the statement, the word remains a partition name. + */ + private boolean isPartitionBinlogModifierAhead() { + int kind = getToken(1).kind; + if (kind != K_NO_WRITE_TO_BINLOG && kind != K_LOCAL) { + return false; + } + Token next = getToken(2); + return next.kind != EOF && next.kind != ST_SEMICOLON && next.kind != K_INTO + && !",".equals(next.image); + } + + /** Whether a PostgreSQL REINDEX statement starts here; REINDEX stays an identifier elsewhere. */ + private boolean isReindexAhead() { + if (getToken(1).kind != K_REINDEX) { + return false; + } + int next = getToken(2).kind; + return next == K_INDEX || next == K_TABLE || next == K_SCHEMA || next == K_DATABASE + || next == K_SYSTEM || "(".equals(getToken(2).image); + } + + /** Whether the next tokens spell an option accepted by this table maintenance operation. */ + private boolean isTableMaintenanceOptionAhead(TableMaintenanceStatement.Operation operation) { + Token token = getToken(1); + TableMaintenanceStatement.Option option = null; + if (token.kind == K_FOR) { + Token next = getToken(2); + if (next.kind == S_IDENTIFIER && "UPGRADE".equalsIgnoreCase(next.image)) { + option = TableMaintenanceStatement.Option.FOR_UPGRADE; + } + } else if (token.kind == K_QUICK) { + option = TableMaintenanceStatement.Option.QUICK; + } else if (token.kind == K_EXTENDED) { + option = TableMaintenanceStatement.Option.EXTENDED; + } else if (token.kind == S_IDENTIFIER) { + option = TableMaintenanceStatement.Option.fromKeyword(token.image); + } + return option != null && operation.allows(option); + } + + private boolean isXmlTypeName(String name) { + return "XML".equalsIgnoreCase(name) || "[XML]".equalsIgnoreCase(name) + || "\"XML\"".equalsIgnoreCase(name); + } + + private boolean isPostgreSqlConstraintAttributeAhead() { + return isPostgreSqlConstraintAttributeAhead(1); + } + + private boolean isPostgreSqlConstraintAttributeAhead(int offset) { + int kind = getToken(offset).kind; + int next = getToken(offset + 1).kind; + return kind == K_DEFERRABLE || kind == K_ENFORCED + || "INITIALLY".equalsIgnoreCase(getToken(offset).image) + || kind == K_NOT && (next == K_DEFERRABLE || next == K_ENFORCED + || "VALID".equalsIgnoreCase(getToken(offset + 1).image)); + } + + /** EXCLUDE starts an exclusion constraint only before USING, its elements or WHERE. */ + private boolean isExcludeConstraintAhead() { + int next = getToken(2).kind; + return getToken(1).kind == K_EXCLUDE + && (next == K_USING || next == OPENING_BRACKET || next == K_WHERE); + } + + private boolean isMySqlStatisticsOptionAhead() { + String name = getToken(1).image; + return "STATS_AUTO_RECALC".equalsIgnoreCase(name) + || "STATS_PERSISTENT".equalsIgnoreCase(name) + || "STATS_SAMPLE_PAGES".equalsIgnoreCase(name); + } + + private boolean isMySqlEngineAttributeAhead() { + return isKeywordAhead("ENGINE_ATTRIBUTE") || isKeywordAhead("SECONDARY_ENGINE_ATTRIBUTE"); + } + + private boolean isMySqlAdditionalTableOptionAhead() { + String name = getToken(1).image.toUpperCase(Locale.ROOT); + return Arrays.asList("SECONDARY_ENGINE", "AUTOEXTEND_SIZE", "INSERT_METHOD", "PACK_KEYS", + "DELAY_KEY_WRITE", "CHECKSUM", "CONNECTION", "COMPRESSION").contains(name); + } + + private boolean isMySqlNumericTableOptionAhead() { + return getToken(1).kind == K_KEY_BLOCK_SIZE || isKeywordAhead("AVG_ROW_LENGTH") + || isKeywordAhead("MAX_ROWS") || isKeywordAhead("MIN_ROWS"); + } + + /** + * PARTITION BY or REMOVE PARTITIONING: the MySQL partition options that may end the ALTER TABLE + * action list without a comma. A column definition does not take them as column options. + */ + private boolean isTrailingPartitionOptionAhead() { + return getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY + || getToken(1).kind == K_REMOVE && getToken(2).kind == K_PARTITIONING; + } + + private boolean isMySqlTableOptionAction(AlterExpression action) { + return action instanceof AlterExpressionTableOption + && ((AlterExpressionTableOption) action).getStructuredTableOption() != null + || action instanceof AlterExpressionCharset + && action.getConvertType() != AlterExpression.ConvertType.CONVERT_TO; + } + + private boolean isMySqlScalarTableOptionAhead() { + int kind = getToken(1).kind; + return isMySqlNumericTableOptionAhead() || isMySqlAdditionalTableOptionAhead() || isMySqlEngineAttributeAhead() || kind == K_ENCRYPTION || kind == K_PASSWORD || kind == K_AUTO_INCREMENT + || (kind == K_DATA || kind == K_INDEX) && getToken(2).kind == K_DIRECTORY; + } + + private boolean isMySqlTableOptionAhead() { + int kind = getToken(1).kind; + if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT || kind == K_TABLESPACE + || isMySqlScalarTableOptionAhead() || kind == K_UNION + || isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT")) { + return true; + } + if (kind == K_CHARACTER && getToken(2).kind == K_SET + || kind == K_CHAR && getToken(2).kind == K_SET) { + return true; + } + if (kind == S_IDENTIFIER && "CHARSET".equalsIgnoreCase(getToken(1).image)) { + return true; + } + if (kind != K_DEFAULT) { + return false; + } + int secondKind = getToken(2).kind; + return secondKind == K_CHARACTER || secondKind == K_CHAR || secondKind == K_COLLATE + || secondKind == S_IDENTIFIER + && "CHARSET".equalsIgnoreCase(getToken(2).image); + } + + private boolean isTableIndexAhead() { + switch (getToken(1).kind) { + case K_PRIMARY: + case K_UNIQUE: + case K_INDEX: + case K_KEY: + case K_FULLTEXT: + case K_SPATIAL: + return true; + default: + return false; + } + } + + /** + * Decides whether an ALTER TABLE ADD table constraint needs the table-constraint production + * shared with CREATE TABLE in dialects that otherwise keep the generic ALTER representation. A + * nested class keeps its branches out of CCJSqlParser.class, whose constant pool is close to + * what JaCoCo can still instrument. + */ + private static final class AlterConstraintLookahead { + private AlterConstraintLookahead() {} + + /** + * Whether the action starting at {@code first} is an unnamed CHECK, a FOREIGN KEY, + * EXCLUDE, or uses a constraint attribute (INITIALLY IMMEDIATE|DEFERRED, NOT VALID, or + * ENFORCED outside CHECK) before the end of the action. + */ + static boolean needsSharedTableConstraint(Token first, CCJSqlParserTokenManager source) { + Token second = next(first, source); + Token body = first.kind == K_CONSTRAINT ? next(second, source) : first; + if (isSharedOnlyConstraint(first, second, body, source)) { + return true; + } + return (first.kind == K_CONSTRAINT || first.kind == K_PRIMARY + || first.kind == K_UNIQUE) + && hasConstraintAttribute(first, body.kind == K_CHECK, source); + } + + /** An unnamed CHECK, a FOREIGN KEY or an EXCLUDE constraint. */ + private static boolean isSharedOnlyConstraint(Token first, Token second, Token body, + CCJSqlParserTokenManager source) { + return first.kind == K_CHECK && second.kind == OPENING_BRACKET + || first.kind == K_FOREIGN || isExcludeConstraint(body, source); + } + + /** Whether a constraint attribute follows at the top level before the end of the action. */ + private static boolean hasConstraintAttribute(Token first, boolean check, + CCJSqlParserTokenManager source) { + int depth = 0; + for (Token token = first; token.kind != EOF; token = next(token, source)) { + if (token.kind == OPENING_BRACKET) { + depth++; + } else if (token.kind == CLOSING_BRACKET) { + if (depth-- == 0) { + return false; + } + } else if (depth == 0) { + if (token.kind == ST_SEMICOLON || token.kind == K_COMMA) { + return false; + } + if (isConstraintAttribute(token, next(token, source), check)) { + return true; + } + } + } + return false; + } + + private static boolean isConstraintAttribute(Token token, Token next, boolean check) { + if ("INITIALLY".equalsIgnoreCase(token.image)) { + return "IMMEDIATE".equalsIgnoreCase(next.image) + || "DEFERRED".equalsIgnoreCase(next.image); + } + return token.kind == K_NOT && "VALID".equalsIgnoreCase(next.image) + || token.kind == K_ENFORCED && !check; + } + + /** EXCLUDE starts an exclusion constraint only before USING, its elements or WHERE. */ + private static boolean isExcludeConstraint(Token token, CCJSqlParserTokenManager source) { + int following = next(token, source).kind; + return token.kind == K_EXCLUDE && (following == K_USING + || following == OPENING_BRACKET || following == K_WHERE); + } + + private static Token next(Token token, CCJSqlParserTokenManager source) { + if (token.next == null) { + token.next = source.getNextToken(); + } + return token.next; + } + } + + /** + * True when an IS after a condition opens a postfix test + * (IS [NOT] TRUE/FALSE/UNKNOWN/NULL/JSON) rather than IS DISTINCT FROM or + * another comparison form, which has a right-hand operand of its own. + */ + private boolean isPostfixIsTestAhead() { + if (getToken(1).kind != K_IS) { + return false; + } + int offset = getToken(2).kind == K_NOT ? 3 : 2; + switch (getToken(offset).kind) { + case K_TRUE: + case K_FALSE: + case K_UNKNOWN: + case K_NULL: + case K_JSON: + return true; + default: + return false; + } + } + + /** + * True when the item after a comma inside a T-SQL/Snowflake ADD list is + * another column definition or table constraint rather than the start of a + * new alter action, which the enclosing productions handle. + */ + private boolean isAlterAddListItemAhead() { + switch (getToken(1).kind) { + case K_CONSTRAINT: + case K_CHECK: + case K_PRIMARY: + case K_UNIQUE: + case K_FOREIGN: + return true; + case K_ADD: + case K_ALTER: + case K_CHANGE: + case K_MODIFY: + case K_DROP: + case K_FORCE: + case K_NO: + case K_ENABLE: + case K_DISABLE: + case K_ALGORITHM: + case K_LOCK: + case K_CONVERT: + case K_PARTITION: + case K_ORDER: + return false; + default: + return true; } + } + + /** + * True when the pending {@code ON} opens the ON CONFLICT clause of the enclosing + * INSERT rather than another trailing join condition: after a join, the trailing + * ON expressions of Issue #1302 and the INSERT clause claim the same ON token. + * The clause shapes are ON CONFLICT DO, ON CONFLICT ON CONSTRAINT and + * ON CONFLICT ( target ) [ WHERE ] DO, so the parenthesized target has to be + * separated from an expression by scanning past its balanced brackets. + */ + private boolean isInsertOnConflictAhead() { + if (getToken(1).kind != K_ON || getToken(2).kind != K_CONFLICT) { + return false; + } + Token t = getToken(3); + if (t.kind == K_DO) { + return true; + } + if (t.kind == K_ON) { + return getToken(4).kind == K_CONSTRAINT; + } + if (!"(".equals(t.image)) { + return false; + } + int depth = 1; + while (depth > 0) { + t = nextWithItemToken(t); + if (t == null || t.kind == EOF) { + return false; + } + if ("(".equals(t.image)) { + depth++; + } else if (")".equals(t.image)) { + depth--; + } + } + t = nextWithItemToken(t); + if (t.kind == K_DO) { + return true; + } + if (t.kind != K_WHERE) { + return false; + } + // the index predicate ends where the conflict action's DO begins + depth = 0; + while (true) { + t = nextWithItemToken(t); + if (t == null || t.kind == EOF) { + return false; + } + if ("(".equals(t.image)) { + depth++; + } else if (")".equals(t.image)) { + depth--; + } else if (t.kind == K_DO && depth == 0) { + return true; + } + } + } + + private static void requireAccessSyntax(boolean condition, String message) throws ParseException { + if (!condition) { throw new ParseException(message); } + } + + /** MySQL role lists name roles with role_ident, which does not take CURRENT_USER(). */ + private static List requireRoleNames(List roles) throws ParseException { + for (UserAccount role : roles) { + requireAccessSyntax(!role.isCurrentUserParentheses(), "A role list does not take CURRENT_USER()"); + } + return roles; + } + + private static > E accessEnum(Class type, String value) throws ParseException { + try { return Enum.valueOf(type, value.toUpperCase(Locale.ROOT)); } + catch (IllegalArgumentException ex) { throw new ParseException("Invalid " + type.getSimpleName() + ": " + value); } + } + + private boolean isRoleOptionAhead(int offset) { + if (getToken(offset).kind == EOF) { return false; } + String word = getToken(offset).image.toUpperCase(Locale.ROOT); + return Arrays.asList("SUPERUSER", "NOSUPERUSER", "CREATEDB", "NOCREATEDB", "CREATEROLE", "NOCREATEROLE", + "INHERIT", "NOINHERIT", "LOGIN", "NOLOGIN", "REPLICATION", "NOREPLICATION", "BYPASSRLS", "NOBYPASSRLS", + "CONNECTION", "ENCRYPTED", "PASSWORD", "VALID", "IN", "ROLE", "USER", "ADMIN", "SYSID").contains(word); + } + + private boolean isPostgreSqlRoleAhead() { + if (isAccessKeywordAhead("ROLE") || isAccessKeywordAhead("GROUP")) { return true; } + return isAccessKeywordAhead("USER") && !"MAPPING".equalsIgnoreCase(getToken(2).image) + && (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + || !isMySqlUserOptionAhead(3) + && ("WITH".equalsIgnoreCase(getToken(3).image) || isRoleOptionAhead(3))); + } + + /** + * Whether a MySQL CREATE/ALTER USER clause starts at the offset: authentication, DEFAULT ROLE, + * REQUIRE, WITH resource limits, password and locking options, COMMENT or ATTRIBUTE. PostgreSQL + * role attributes such as WITH LOGIN or PASSWORD 'x' do not match. + */ + private boolean isMySqlUserOptionAhead(int offset) { + Token token = getToken(offset); + switch (token.kind) { + case K_IDENTIFIED: + case K_DISCARD: + return true; + case K_COMMENT: + return getToken(offset + 1).kind == S_CHAR_LITERAL; + case K_DEFAULT: + return "ROLE".equalsIgnoreCase(getToken(offset + 1).image); + case K_WITH: + return isMySqlResourceLimitAhead(offset + 1); + default: + return isMySqlPasswordLockOptionAhead(offset) || token.kind == S_IDENTIFIER + && ("REQUIRE".equalsIgnoreCase(token.image) + || "ATTRIBUTE".equalsIgnoreCase(token.image)); + } + } + + private boolean isMySqlResourceLimitAhead(int offset) { + Token token = getToken(offset); + return token.kind == S_IDENTIFIER && getToken(offset + 1).kind == S_LONG + && Arrays.asList("MAX_QUERIES_PER_HOUR", "MAX_UPDATES_PER_HOUR", + "MAX_CONNECTIONS_PER_HOUR", "MAX_USER_CONNECTIONS") + .contains(token.image.toUpperCase(Locale.ROOT)); + } + + private boolean isMySqlPasswordLockOptionAhead(int offset) { + Token token = getToken(offset); + Token next = getToken(offset + 1); + if (token.kind == K_PASSWORD) { + return next.kind == K_HISTORY || next.kind == S_IDENTIFIER + && Arrays.asList("EXPIRE", "REUSE", "REQUIRE") + .contains(next.image.toUpperCase(Locale.ROOT)); + } + if (token.kind != S_IDENTIFIER) { + return false; + } + String word = token.image.toUpperCase(Locale.ROOT); + return "ACCOUNT".equals(word) + && (next.kind == K_LOCK || "UNLOCK".equalsIgnoreCase(next.image)) + || "FAILED_LOGIN_ATTEMPTS".equals(word) || "PASSWORD_LOCK_TIME".equals(word); + } + + private boolean isMySqlTlsOptionAhead() { + int offset = getToken(1).kind == K_AND ? 2 : 1; + Token token = getToken(offset); + return token.kind == S_IDENTIFIER && Arrays.asList("SSL", "X509", "CIPHER", "ISSUER", + "SUBJECT").contains(token.image.toUpperCase(Locale.ROOT)); + } + + /** An unsupported CREATE statement that keeps OR REPLACE, which Create() has already read. */ + private static UnsupportedStatement unsupportedCreate(boolean orReplace, List rest) { + List declarations = new ArrayList(); + if (orReplace) { + declarations.add("OR"); + declarations.add("REPLACE"); + } + declarations.addAll(rest); + return new UnsupportedStatement("CREATE", declarations); + } + + /** + * Whether a CREATE statement names none of TABLE, INDEX, VIEW and POLICY before it ends. Every + * remaining CREATE form needs one of them, and CREATE TABLE and CREATE INDEX accept any leading + * words as modifiers, so such a statement would fail there instead of reaching the fallback. + */ + private boolean isUnknownCreateAhead() { + for (int i = 1; ; i++) { + int kind = getToken(i).kind; + if (kind == EOF || kind == ST_SEMICOLON) { return true; } + if (kind == K_TABLE || kind == K_INDEX || kind == K_VIEW || kind == K_POLICY) { return false; } + } + } + + /** + * Whether ALTER USER uses MySQL account syntax rather than the PostgreSQL alias of ALTER ROLE, + * which names one unquoted role followed by an action: IF EXISTS, USER(), CURRENT_USER(), a + * quoted name, a host, an account list, no action at all, or a MySQL account clause. + */ + private boolean isMySqlAlterUserAhead() { + if (getToken(1).kind != K_USER) { return false; } + Token name = getToken(2); + if (name.kind == K_IF || name.kind == S_CHAR_LITERAL + || (name.kind == K_USER || "CURRENT_USER".equalsIgnoreCase(name.image)) + && "(".equals(getToken(3).image) + || isMySqlAccountHostAhead(2, false)) { + return true; + } + Token next = getToken(3); + return ",".equals(next.image) || next.kind == EOF || next.kind == ST_SEMICOLON + || isMySqlUserOptionAhead(3); + } + + /** + * Whether CREATE ROLE uses MySQL syntax: IF NOT EXISTS, a quoted name, a host or a list. An + * unquoted name@host, which PostgreSQL reads as one role name, counts only in the MySQL and + * MariaDB dialects. + */ + private boolean isMySqlCreateRoleAhead() { + if (!isAccessKeywordAhead("ROLE")) { return false; } + Token name = getToken(2); + return name.kind == K_IF || name.kind == S_CHAR_LITERAL + || isMySqlAccountHostAhead(2, isMySqlDialect()) || ",".equals(getToken(3).image); + } + + /** + * Whether the account name at the offset has a host that only MySQL accepts: @ before the host, + * a user@ identifier before a quoted host or an @host identifier after a quoted user. An unquoted + * user@host, which the lexer reads as one identifier, counts only when merged is set. + */ + private boolean isMySqlAccountHostAhead(int offset, boolean merged) { + return isMySqlAccountHost(getToken(offset), merged); + } + + private boolean isMySqlAccountHost(Token name, boolean merged) { + if (name.kind == S_IDENTIFIER && name.image.indexOf('@') > 0) { + return merged || name.image.endsWith("@"); + } + Token next = nextWithItemToken(name); + return next.kind == K_AT_SIGN + || next.kind == S_AT_IDENTIFIER && !next.image.startsWith("@@"); + } + + /** + * Whether a MySQL SET ROLE statement starts here. The MySQL and MariaDB dialects type every + * form; elsewhere only ALL EXCEPT and role names with a quoted user or host do, so that + * SET ROLE name and SET ROLE NONE remain the SetStatement that PostgreSQL uses. + */ + private boolean isMySqlSetRoleAhead() { + if (getToken(1).kind != K_SET || getToken(2).kind != S_IDENTIFIER + || !"ROLE".equalsIgnoreCase(getToken(2).image)) { + return false; + } + Token next = getToken(3); + if (next.kind == EOF || next.kind == ST_SEMICOLON || next.kind == K_TO + || "=".equals(next.image) || ":=".equals(next.image) || ".".equals(next.image)) { + return false; + } + if (isMySqlDialect()) { return true; } + if (next.kind == K_ALL) { return getToken(4).kind == K_EXCEPT; } + // role [@ host] [, role [@ host]] ... + for (Token role = next; ; role = nextWithItemToken(nextWithItemToken(role))) { + if (isMySqlAccountHost(role, false)) { return true; } + if (!",".equals(nextWithItemToken(role).image)) { return false; } + } + } + + /** + * Whether a MySQL SET PASSWORD statement starts here: always with FOR, otherwise only in the + * MySQL and MariaDB dialects, where SET PASSWORD = 'x' is not a variable assignment. + */ + private boolean isMySqlSetPasswordAhead() { + if (getToken(1).kind != K_SET || getToken(2).kind != K_PASSWORD) { return false; } + Token next = getToken(3); + return next.kind == K_FOR + || isMySqlDialect() && ("=".equals(next.image) || next.kind == K_TO); + } + + private boolean isAccessKeywordAhead(String word) { + return getToken(1).kind != EOF && isKeywordAhead(word); + } + + private boolean isAccessTargetKindAhead() { + Token next = getToken(2); + return next.kind != EOF && next.kind != K_TO && next.kind != K_FROM + && !",".equals(next.image) && !".".equals(next.image) + && isAccessTargetKindAhead(1); + } + + private boolean isAccessTargetKindAhead(int offset) { + return Arrays.asList("TABLE", "SEQUENCE", "DATABASE", "DOMAIN", "FOREIGN", "FUNCTION", "PROCEDURE", + "ROUTINE", "LANGUAGE", "LARGE", "PARAMETER", "SCHEMA", "TABLESPACE", "TYPE") + .contains(getToken(offset).image.toUpperCase(Locale.ROOT)); + } + + private boolean isObjectPrivilegeAhead() { + int depth = 0; + for (Token token = getToken(1); ; token = nextWithItemToken(token)) { + if (token.kind == EOF || ";".equals(token.image)) { return false; } + if ("(".equals(token.image)) { depth++; } + else if (")".equals(token.image)) { depth--; } + else if (depth == 0) { + if (token.kind == K_ON) { return true; } + if (token.kind == K_TO || token.kind == K_FROM) { return false; } + } + } + } + + /** MySQL {@code REVOKE ALL [PRIVILEGES], GRANT OPTION FROM ...}, which has no ON clause. */ + private boolean isAccessAllGrantOptionAhead() { + if (getToken(1).kind != K_ALL) { return false; } + int i = "PRIVILEGES".equalsIgnoreCase(getToken(2).image) ? 3 : 2; + return ",".equals(getToken(i).image) && getToken(i + 1).kind == K_GRANT + && getToken(i + 2).kind == K_OPTION && getToken(i + 3).kind == K_FROM; + } + + /** PUBLIC, CURRENT_USER, CURRENT_ROLE or SESSION_USER, unless used as the user of user@host. */ + private boolean isAccessSpecialGranteeAhead() { + Token token = getToken(1); + int next = getToken(2).kind; + return (token.kind == K_PUBLIC || token.kind == S_IDENTIFIER) + && next != K_AT_SIGN && next != S_AT_IDENTIFIER + && Arrays.asList("PUBLIC", "CURRENT_USER", "CURRENT_ROLE", "SESSION_USER") + .contains(token.image.toUpperCase(Locale.ROOT)); + } + + /** Privileges take only GRANT OPTION; role memberships take distinct ADMIN, INHERIT and SET options. */ + private static boolean isValidAccessGrantOptions(PrivilegeClause clause, List options) { + if (clause.getPrivileges() != null) { + return options.size() == 1 && options.get(0).getKind() == GrantOption.Kind.GRANT + && options.get(0).getValue() == GrantOption.Value.OPTION; + } + EnumSet kinds = EnumSet.noneOf(GrantOption.Kind.class); + for (GrantOption option : options) { + if (option.getKind() == GrantOption.Kind.GRANT || !kinds.add(option.getKind())) { return false; } + } + return true; + } + + private boolean isPostgreSqlTriggerAhead() { + if (getToken(1).kind == K_CONSTRAINT) { return true; } + if (getToken(1).kind != K_TRIGGER) { return false; } + for (Token token = getToken(2); ; token = nextWithItemToken(token)) { + if (token.kind == EOF || ";".equals(token.image) || token.kind == K_BEGIN) { return false; } + if (token.kind == K_EXECUTE && (nextWithItemToken(token).kind == K_FUNCTION || nextWithItemToken(token).kind == K_PROCEDURE)) { return true; } + // DuckDB 2.0 uses the standard header with a statement body instead of EXECUTE + if ("REFERENCING".equalsIgnoreCase(token.image) || token.kind == K_STATEMENT) { return true; } + } + } + + /** + * Whether EVENT TRIGGER starts a PostgreSQL event trigger rather than a MySQL event named + * trigger: the trigger name is followed by ON in CREATE and by an action keyword in ALTER. + */ + private boolean isPostgreSqlEventTriggerAhead(boolean alter) { + if (getToken(1).kind != K_EVENT || getToken(2).kind != K_TRIGGER + || ".".equals(getToken(3).image)) { + return false; + } + Token next = getToken(4); + if (!alter) { + return next.kind == K_ON; + } + return next.kind == K_ENABLE || next.kind == K_DISABLE || next.kind == K_RENAME + || next.kind == S_IDENTIFIER && "OWNER".equalsIgnoreCase(next.image); + } + + // In these dialects -> always denotes JSON access, even after a comma or parentheses. + private boolean isJsonArrowDialect() { + return Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + || isMySqlDialect(); + } + + private boolean isMySqlDialect() { + String dialect = getAsString(Feature.dialect); + return Dialect.MYSQL.name().equals(dialect) || Dialect.MARIADB.name().equals(dialect); + } + + /** + * Whether ALTER TABLE DROP names a MySQL FOREIGN KEY or CHECK constraint. The default + * configuration accepts both like MySQL; MariaDB has no DROP CHECK. A parenthesized FOREIGN KEY + * column list keeps the legacy form, and other dialects use DROP CONSTRAINT. + */ + private boolean isNamedConstraintDropAhead() { + String dialect = getAsString(Feature.dialect); + boolean namedForeignKey = getToken(1).kind == K_FOREIGN && getToken(2).kind == K_KEY + && getToken(3).kind != OPENING_BRACKET; + if (dialect == null || Dialect.MYSQL.name().equals(dialect)) { + return namedForeignKey || getToken(1).kind == K_CHECK; + } + return Dialect.MARIADB.name().equals(dialect) && namedForeignKey; + } + + private boolean isBeginTransactionAhead() { + String dialect = getAsString(Feature.dialect); + if (getToken(1).kind != K_BEGIN || !(isMySqlDialect() + || Dialect.POSTGRESQL.name().equals(dialect))) { return false; } + Token next = getToken(2); + return next.kind == EOF || next.kind == ST_SEMICOLON || next.kind == K_WORK + || next.kind == K_READ || next.kind == K_DEFERRABLE || next.kind == K_NOT + || "TRANSACTION".equalsIgnoreCase(next.image) + || "ISOLATION".equalsIgnoreCase(next.image); + } + + private boolean isTransactionModeAhead() { + return isKeywordAhead("ISOLATION") || getToken(1).kind == K_READ + || getToken(1).kind == K_DEFERRABLE + || getToken(1).kind == K_NOT && getToken(2).kind == K_DEFERRABLE + || getToken(1).kind == K_WITH && "CONSISTENT".equalsIgnoreCase(getToken(2).image); + } + + private boolean isMySqlSelectModifierAhead() { + Token token = getToken(1); + // These existing modifiers remain available without a dialect preset. + if (token.kind == K_STRAIGHT || token.kind == K_SQL_CACHE + || token.kind == K_SQL_CALC_FOUND_ROWS) { + return true; + } + // Keep qualified names, aliases and standalone identifier expressions intact. + int next = getToken(2).kind; + if (next == K_AS || next == K_FROM || next == EOF || next == ST_SEMICOLON + || ".".equals(getToken(2).image) || ",".equals(getToken(2).image) + || ")".equals(getToken(2).image)) { + return false; + } + // SQL_NO_CACHE is a non-reserved word, so it is a modifier only before a select item. + if (token.kind == K_SQL_NO_CACHE) { + return true; + } + if (getAsString(Feature.dialect) != null && !isMySqlDialect()) { + return false; + } + return token.kind == K_HIGH_PRIORITY || token.kind == S_IDENTIFIER + && ("SQL_SMALL_RESULT".equalsIgnoreCase(token.image) + || "SQL_BIG_RESULT".equalsIgnoreCase(token.image) + || "SQL_BUFFER_RESULT".equalsIgnoreCase(token.image)); + } + + private boolean isMySqlSelectIntoAhead() { + if (getToken(1).kind != K_INTO) { return false; } + int kind = getToken(2).kind; + return kind == K_OUTFILE || kind == K_DUMPFILE + || isMySqlDialect() && (kind == S_AT_IDENTIFIER || kind == K_AT_SIGN); + } + + private static UserVariable requireMySqlIntoVariable(UserVariable variable) throws ParseException { + if (variable.isDoubleAdd()) { + throw new ParseException("INTO requires a user variable with a single @ prefix"); + } + return variable; + } + + private ExplainStatement.OptionType postgresqlExplainOptionType(String name) throws ParseException { + try { + return ExplainStatement.OptionType.from(name); + } catch (IllegalArgumentException ex) { + throw new ParseException("Unknown PostgreSQL EXPLAIN option: " + name); + } + } + + private void validatePostgresqlExplain(ExplainStatement explain) throws ParseException { + if (explain.getTable() != null) { + throw new ParseException("PostgreSQL EXPLAIN requires a statement"); + } + int previous = -1; + for (ExplainStatement.Option option : explain.getOptionList()) { + ExplainStatement.OptionType type = option.getType(); + String value = option.getValue(); + if (!explain.isParenthesizedOptions()) { + int position = type == ExplainStatement.OptionType.ANALYZE ? 0 + : type == ExplainStatement.OptionType.VERBOSE ? 1 : -1; + if (position <= previous || value != null) { + throw new ParseException("PostgreSQL EXPLAIN options require parentheses"); + } + previous = position; + } else { + boolean valid; + switch (type) { + case FORMAT: + valid = value != null && value.matches("(?i)TEXT|XML|JSON|YAML"); + break; + case SERIALIZE: + valid = value == null || value.matches("(?i)NONE|TEXT|BINARY"); + break; + case PLAN: case PLAN_FOR: + valid = false; + break; + default: + valid = value == null || value.matches("(?i)TRUE|FALSE|ON|OFF|0|1"); + } + if (!valid) { + throw new ParseException("Invalid PostgreSQL EXPLAIN option: " + option.formatOption()); + } + } + } + } + + private void requireDdlSyntax(boolean valid, String message) throws ParseException { + if (!valid) { + throw new ParseException(message); + } + } + + private LikeClause.OptionKind likeOptionKind(String name) throws ParseException { + try { + return LikeClause.OptionKind.valueOf(name.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + throw new ParseException("Unknown LIKE option: " + name); + } + } + + private boolean isPostgreSqlRelationActionAhead() { + Token first = getToken(1); + if ((first.kind == K_ENABLE || first.kind == K_DISABLE) + && ("RULE".equalsIgnoreCase(getToken(2).image) + || (getToken(2).kind == K_ALWAYS || "REPLICA".equalsIgnoreCase(getToken(2).image)) + && "RULE".equalsIgnoreCase(getToken(3).image))) { return true; } + int columnActionOffset = getToken(2).kind == K_COLUMN ? 4 : 3; + if (first.kind == K_ALTER && getToken(columnActionOffset).kind == K_SET + && "EXPRESSION".equalsIgnoreCase(getToken(columnActionOffset + 1).image)) { + return true; + } + // VALIDATE CONSTRAINT starts no other ALTER TABLE action in any dialect. + if (first.kind == K_VALIDATE && getToken(2).kind == K_CONSTRAINT) { return true; } + if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { return false; } + if (first.kind == K_VALIDATE || first.kind == K_RESET || isKeywordAhead("INHERIT") + || isKeywordAhead("REPLICA") || isKeywordAhead("OWNER") || isKeywordAhead("CLUSTER") + || first.kind == K_OF || first.kind == K_NOT && getToken(2).kind == K_OF + || (first.kind == K_ENABLE || first.kind == K_DISABLE) + && (getToken(2).kind == K_TRIGGER || getToken(2).kind == K_ALWAYS + || "REPLICA".equalsIgnoreCase(getToken(2).image))) { return true; } + if (first.kind == K_NO) { return "INHERIT".equalsIgnoreCase(getToken(2).image); } + if (first.kind == K_SET) { + return getToken(2).kind == OPENING_BRACKET || getToken(2).kind == K_SCHEMA + || getToken(2).kind == K_TABLESPACE || "ACCESS".equalsIgnoreCase(getToken(2).image) + || getToken(2).kind == K_WITHOUT || getToken(2).kind == K_UNLOGGED + || "LOGGED".equalsIgnoreCase(getToken(2).image); + } + if (first.kind != K_ALTER) { return false; } + if (getToken(2).kind == K_CONSTRAINT) { + return "INHERIT".equalsIgnoreCase(getToken(4).image) + || getToken(4).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(5).image); + } + int offset = getToken(2).kind == K_COLUMN ? 4 : 3; + return getToken(offset).kind == K_RESET + || getToken(offset).kind == K_DROP && "EXPRESSION".equalsIgnoreCase(getToken(offset + 1).image) + || getToken(offset).kind == K_SET && (getToken(offset + 1).kind == OPENING_BRACKET + || getToken(offset + 1).kind == K_STORAGE + || "COMPRESSION".equalsIgnoreCase(getToken(offset + 1).image) + || "STATISTICS".equalsIgnoreCase(getToken(offset + 1).image)); + } + + private void validateRelationAction(AlterRelation.ObjectType type, RelationAlterAction action) throws ParseException { + RelationAlterAction.Kind kind = action.getKind(); + EnumSet allowed = EnumSet.of(RelationAlterAction.Kind.RENAME, + RelationAlterAction.Kind.SET_OPTIONS, RelationAlterAction.Kind.RESET_OPTIONS, + RelationAlterAction.Kind.ALTER_COLUMN); + if (type == AlterRelation.ObjectType.INDEX) { + allowed.addAll(EnumSet.of(RelationAlterAction.Kind.SET_TABLESPACE, + RelationAlterAction.Kind.ATTACH_PARTITION, RelationAlterAction.Kind.DEPENDS_ON_EXTENSION)); + } else { + allowed.addAll(EnumSet.of(RelationAlterAction.Kind.RENAME_COLUMN, + RelationAlterAction.Kind.OWNER, RelationAlterAction.Kind.SET_SCHEMA)); + if (type == AlterRelation.ObjectType.MATERIALIZED_VIEW) { + allowed.addAll(EnumSet.of(RelationAlterAction.Kind.SET_TABLESPACE, + RelationAlterAction.Kind.SET_ACCESS_METHOD, RelationAlterAction.Kind.DEPENDS_ON_EXTENSION, + RelationAlterAction.Kind.CLUSTER_ON, RelationAlterAction.Kind.SET_WITHOUT_CLUSTER)); + } + } + requireDdlSyntax(allowed.contains(kind), "Unsupported " + type + " action: " + kind); + if (kind == RelationAlterAction.Kind.ALTER_COLUMN) { + RelationAlterAction.ColumnAction column = action.getColumnAction(); + if (type == AlterRelation.ObjectType.INDEX) { + requireDdlSyntax(action.getColumnNumber() != null + && column == RelationAlterAction.ColumnAction.SET_STATISTICS, + "ALTER INDEX column actions require an ordinal and SET STATISTICS"); + } else { + requireDdlSyntax(action.getColumnNumber() == null, "Expected a column name"); + boolean defaults = column == RelationAlterAction.ColumnAction.SET_DEFAULT + || column == RelationAlterAction.ColumnAction.DROP_DEFAULT; + requireDdlSyntax(type == AlterRelation.ObjectType.VIEW ? defaults + : column == RelationAlterAction.ColumnAction.SET_STATISTICS + || column == RelationAlterAction.ColumnAction.SET_STORAGE + || column == RelationAlterAction.ColumnAction.SET_COMPRESSION + || column == RelationAlterAction.ColumnAction.SET_OPTIONS + || column == RelationAlterAction.ColumnAction.RESET_OPTIONS, + "Unsupported view column action: " + column); + } + } + } + + /** Whether a MySQL tablespace option, which reuses a table option model, starts here. */ + private boolean isMySqlTablespaceOptionAhead(int offset) { + Token token = getToken(offset); + if (token.kind == K_ENGINE || token.kind == K_ENCRYPTION) { + return true; + } + return token.kind == S_IDENTIFIER && ("AUTOEXTEND_SIZE".equalsIgnoreCase(token.image) + || "FILE_BLOCK_SIZE".equalsIgnoreCase(token.image) + || "ENGINE_ATTRIBUTE".equalsIgnoreCase(token.image)); + } + + /** Only CREATE TABLESPACE sets FILE_BLOCK_SIZE, and undo tablespaces accept only ENGINE. */ + private TableOption validateTablespaceOption(TableOption option, boolean create, boolean undo) + throws ParseException { + TableOption.Kind kind = option.getKind(); + requireDdlSyntax(kind == TableOption.Kind.ENGINE + || !undo && (create || kind != TableOption.Kind.FILE_BLOCK_SIZE), + (create ? "CREATE " : "ALTER ") + (undo ? "UNDO " : "") + "TABLESPACE does not accept " + + option.getName()); + return option; + } + + private boolean isIdentityAlterationAhead() { + int first = getToken(1).kind; + int second = getToken(2).kind; + if ((first == K_ADD || first == K_SET) + && "GENERATED".equalsIgnoreCase(getToken(2).image)) { + return true; + } + if (first == K_RESTART || (first == K_DROP && second == K_IDENTITY)) { + return true; + } + if (first == K_SET) { + switch (second) { + case K_INCREMENT: case K_START: case K_MINVALUE: case K_MAXVALUE: + case K_NO: case K_CACHE: case K_CYCLE: + return true; + default: + return false; + } + } + return false; + } + + /** Attaches the definition to its own AST slot and populates remaining legacy metadata. */ + private void setAlterTableElement(AlterExpression alterExp, TableElement element) { + if (element instanceof Index) { + alterExp.setIndex((Index) element); + return; + } + NamedConstraint constraint = (NamedConstraint) element; + alterExp.setConstraint(constraint); + if (constraint instanceof KeyConstraint && constraint.getKind() == ConstraintKind.UNIQUE) { + KeyConstraint key = (KeyConstraint) constraint; + // Preserve generic ALTER's historical placement of USING and COMMENT options. + if (!Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) + && !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { + for (IndexSpecOption option : new ArrayList(key.getSpecOptions())) { + if (option.getKind() == IndexSpecOption.Kind.INDEX_TYPE) { + alterExp.addParameters("USING", option.getValue()); + key.getSpecOptions().remove(option); + } else if (option.getKind() == IndexSpecOption.Kind.COMMENT) { + key.setCommentText(option.getValue()); + key.getSpecOptions().remove(option); + } + } + } + } + } + + /** Keeps relation-option branches out of the generated parser's nearly full constant pool. */ + private static final class RelationOptions { + private RelationOptions() {} + + static LockStatement.Target lockTarget(TableReference reference) { + LockStatement.Target target = new LockStatement.Target(reference.getTable()); + target.setOnly(reference.isOnly()); + target.setIncludeDescendants(reference.isIncludeDescendants()); + target.setParenthesized(reference.isParenthesized()); + return target; + } + + static boolean isOnlyAhead(Token prev, int next) { + boolean name = next == S_IDENTIFIER || next == S_QUOTED_IDENTIFIER || next == OPENING_BRACKET; + switch (prev.kind) { + case K_FROM: case K_JOIN: case K_UPDATE: case K_INTO: case K_USING: + return name || next >= MIN_NON_RESERVED_WORD && next <= MAX_NON_RESERVED_WORD; + case K_COMMA: + // a column definition continues with its type, as in (a INT, only INT) + return name; + default: + return false; + } + } + + static void apply(Truncate truncate, String log, String storage, + String identity, String behavior) { + if (log != null) { + truncate.setMaterializedViewLogOption(Truncate.MaterializedViewLogOption.valueOf(log)); + } + if (storage != null) { + truncate.setStorageOption(Truncate.StorageOption.valueOf(storage)); + } + if (identity != null) { + truncate.setIdentityOption(Truncate.IdentityOption.valueOf(identity)); + } + if (behavior != null) { + truncate.setDropBehavior(CascadeBehavior.valueOf(behavior)); + } + } + } + + private void requireNoDropOwnerTable(Table owner) throws ParseException { + if (owner != null) { + throw new ParseException("Duplicate ON clause in DROP"); + } + } + + /** Keeps DROP lookahead branches out of the generated parser's nearly full constant pool. */ + private static final class DropLookahead { + private DropLookahead() {} + + /** SQL Server DDL triggers name a scope rather than an owning table. */ + static boolean isOwnerTable(Drop drop, Table name) { + String text = name.getFullyQualifiedName(); + boolean triggerScope = drop.getObjectType() == Drop.ObjectType.TRIGGER + && ("DATABASE".equalsIgnoreCase(text) || "ALL".equalsIgnoreCase(text)); + return drop.getObjectType().hasOwnerTable() && !triggerScope; + } + + static boolean isForceAhead(Drop drop, Token first, Token second, Token third) { + return drop.getObjectType() == Drop.ObjectType.SYNONYM && first.kind == K_FORCE + || drop.getObjectType() == Drop.ObjectType.DATABASE + && (first.kind == K_WITH && "(".equals(second.image) && third.kind == K_FORCE + || "(".equals(first.image) && second.kind == K_FORCE); + } + } + + /** Builds names from lexer-separated components without reinterpreting PostgreSQL dots. */ + protected Table createTable(List names) { + return Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) && names.size() == 1 + ? new Table(names.get(0), false) : new Table(names); + } + + protected Column createColumn(List names, List delimiters) { + if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { + return new Column(names, delimiters); + } + Column column = new Column(); + column.setName(names.get(names.size() - 1), false); + if (names.size() > 1) { + column.setTable(createTable(names.subList(0, names.size() - 1))); + } + column.setTableDelimiter(delimiters.isEmpty() ? "." : delimiters.get(delimiters.size() - 1)); + return column; + } + + protected Column createColumn(String name) { + return createColumn(Collections.singletonList(name), Collections.emptyList()); + } + + /** + * Lightweight lookahead for SpecialStringFunctionWithNamedParameters: + * scans forward from the current position (just past the opening '(') + * looking for FROM, IN, or PLACING at bracket nesting depth 0. + * + * This replaces an expensive syntactic LOOKAHEAD(NamedExpressionListExprFirst()) + * that caused exponential backtracking with deeply nested expressions. + */ + protected boolean isNamedExprListAhead() { + int depth = 0; + for (Token t = getToken(1); ; t = nextWithItemToken(t)) { + if (t.kind == EOF) { + return false; + } + if (t.kind == OPENING_BRACKET) { + depth++; + } else if (t.kind == CLOSING_BRACKET) { + if (depth == 0) { + return false; + } + depth--; + } else if (depth == 0) { + if (t.kind == K_FROM || t.kind == K_IN || t.kind == K_PLACING) { + return true; + } + if (t.image.equals(",")) { + return false; + } + } + } + } + + /** + * Creates a string literal that remembers how this configuration lexed its backslashes. A + * PostgreSQL escape string with a Unicode escape that the server rejects is rejected as well. + */ + private StringValue stringValue(Token token) throws ParseException { + StringValue value = unlinkedStringValue(token); + linkAST(value, token, token); + return value; + } + + private StringValue unlinkedStringValue(Token token) throws ParseException { + StringValue value = new StringValue(token.image); + // a U&'...' string brings its own escaping + if (value.getQuoteStr() == null || value.getQuoteStr().startsWith("$") + || value.getEscaping() == StringValue.Escaping.POSTGRESQL_UNICODE_ESCAPE) { + return value; + } + String dialect = getAsString(Feature.dialect); + boolean backslashes = getAsBoolean(Feature.allowBackslashEscapeCharacter); + if (Dialect.POSTGRESQL.name().equals(dialect)) { + if (backslashes || "E".equalsIgnoreCase(value.getPrefix())) { + value.setEscaping(StringValue.Escaping.POSTGRESQL_ESCAPE_STRING); + try { + value.getNotExcapedValue(); + } catch (IllegalArgumentException e) { + throw new ParseException("Invalid escape string " + token.image + ": " + + e.getMessage()); + } + } + } else if (backslashes && (dialect == null || isMySqlDialect())) { + value.setEscaping(StringValue.Escaping.MYSQL_BACKSLASH); + } + return value; + } + + /** + * Semantic lookahead for the adjacent string literal concatenation in PrimaryExpression: + * true when Feature.adjacentStringLiterals is on and the next token is a string literal + * meeting the mode's separation rule (WHITESPACE: any, NEWLINE: a newline between the + * tokens, detected through the token line numbers). + */ + protected boolean isAdjacentStringConcat() { + String mode = getAsString(Feature.adjacentStringLiterals); + if (mode == null || AdjacentStringLiterals.OFF.name().equals(mode)) { + return false; + } + if (getToken(1).kind != S_CHAR_LITERAL) { + return false; + } + if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && (StringValue.getDollarQuoteDelimiter(getToken(0).image) != null + || !getToken(1).image.startsWith("'"))) { + return false; + } + return AdjacentStringLiterals.WHITESPACE.name().equals(mode) + || getToken(1).beginLine > getToken(0).endLine; + } + + /** + * Checks if the next token can start a condition suffix + * (comparison, IN, BETWEEN, LIKE, IS NULL, etc.) + * + * Used as the entry guard for the entire optional condition-suffix block + * in Condition(), eliminating choice conflicts. + */ + protected boolean isConditionSuffixAhead() { + Token t = getToken(1); + if (isLikeOperator(t)) return true; + // Inline isComparisonOperatorAhead for single getToken(1) call + if (t.kind == OPENING_BRACKET && getToken(2).image.equals("+")) { + return isComparisonOperator(getToken(4)); + } + // ClickHouse ternary `cond ? then : else` — the `?` must not be taken as the + // PostgreSQL JSON operator, it is consumed by prattExpressionRest instead + if ("?".equals(t.image) && isTernaryAhead()) return false; + if (isComparisonOperator(t)) return true; + return isConditionKeywordAhead(1); + } + + private boolean isConditionKeywordAhead(int offset) { + switch (getToken(offset).kind) { + // Each suffix's start token: + case K_OVERLAPS: // OVERLAPS + case K_IN: // IN + case K_GLOBAL: // GLOBAL ... IN + case K_EXCLUDES: // EXCLUDES (...) + case K_INCLUDES: // INCLUDES (...) + case K_BETWEEN: // BETWEEN + case K_MEMBER: // MEMBER OF + case K_IS: // IS [NOT] NULL / TRUE / FALSE / UNKNOWN / DISTINCT + case K_ISNULL: // ISNULL + case K_NOTNULL: // NOTNULL + case K_LIKE: // LIKE + case K_ILIKE: // ILIKE + case K_RLIKE: // RLIKE + case K_REGEXP_LIKE: // REGEXP_LIKE + case K_REGEXP: // REGEXP + case K_GLOB: // GLOB (DuckDB/SQLite) + case K_SIMILAR_TO: // SIMILAR TO (in LikeExpression) + case K_SIMILAR: // SIMILAR TO (in SimilarToExpression) + case K_MATCH_ANY: // MATCH_ANY + case K_MATCH_ALL: // MATCH_ALL + case K_MATCH_PHRASE: // MATCH_PHRASE + case K_MATCH_PHRASE_PREFIX: // MATCH_PHRASE_PREFIX + case K_MATCH_REGEXP: // MATCH_REGEXP + return true; + case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR + // A column's NOT NULL constraint starts after its DEFAULT expression. + return getToken(offset + 1).kind != K_NULL; + // Oracle (+) before IN: col(+) IN (...) + case OPENING_BRACKET: + return getToken(offset + 1).image.equals("+"); + default: + return false; + } + } + + private boolean isLargeObjectTypeAhead() { + int offset = 1; + if (isKeywordAhead("NATIONAL")) { + offset++; + if (!"CHARACTER".equalsIgnoreCase(getToken(offset).image)) { + return false; + } + } + String name = getToken(offset).image; + return ("CHARACTER".equalsIgnoreCase(name) || "CHAR".equalsIgnoreCase(name) + || "NCHAR".equalsIgnoreCase(name) || "BINARY".equalsIgnoreCase(name)) + && "LARGE".equalsIgnoreCase(getToken(offset + 1).image) + && "OBJECT".equalsIgnoreCase(getToken(offset + 2).image); + } + + /** + * Extracts the numeric precision embedded in a DT_ZONE token image such as + * "TIMESTAMP(3) WITH TIME ZONE", or null when the image carries no parameter. + */ + private static Integer zonedTypePrecision(String image) { + int open = image.indexOf('('); + if (open < 0) { + return null; + } + return Integer.valueOf(image.substring(open + 1, image.indexOf(')', open)).trim()); + } + +} + +PARSER_END(CCJSqlParser) + +TOKEN_MGR_DECLS : { + public FeatureConfiguration configuration = new FeatureConfiguration(); + + + // Identify the index of the quoting/escaping tokens + public int charLiteralIndex = -1; + public int squaredBracketOpenIndex = -1; + { + for (int i=0;i") ) { + charLiteralIndex = i; + break; + } + } + for (int i=0;i 1 && (s.charAt(0) == 'q' || s.charAt(0) == 'Q') && s.charAt(1) == '\'' + || isUnicodeEscapeString(s)) { + return -1; + } + return closingQuote(s, s.indexOf('\''), '\'', backslashEscapes); + } + + private static boolean isUnicodeEscapeString(String s) { + return s.regionMatches(true, 0, "U&'", 0, 3); + } + + // A PostgreSQL Unicode escape string U&'...' [UESCAPE 'c'] is one literal with + // Dialect.POSTGRESQL, which rejects invalid escapes as the server does. Other dialects keep + // reading the identifier U followed by the & operator. + private void lexUnicodeEscapeString(Token token) { + if (!AbstractJSqlParser.Dialect.POSTGRESQL.name().equals(configuration.getValue(Feature.dialect))) { + truncateIdentifierToken(token, 1); + token.kind = S_IDENTIFIER; + return; + } + try { + new StringValue(token.image).getNotExcapedValue(); + } catch (IllegalArgumentException e) { + // Rewind so that every later attempt to read past the literal fails as well + input_stream.backup(token.image.length()); + throw new TokenMgrException("Lexical error at line " + token.beginLine + ", column " + + token.beginColumn + ". Invalid Unicode escape string " + token.image + ": " + + e.getMessage(), TokenMgrException.LEXICAL_ERROR); + } + } + + private RowPatternTokenContext rowPatternContext; + + private void reportMySqlCommentError() { + throw new TokenMgrException(input_stream.mySqlCommentError == null + ? "Unexpected control character" : input_stream.mySqlCommentError, + TokenMgrException.LEXICAL_ERROR); + } + + public void CommonTokenAction(Token t) + { + t.absoluteBegin = getCurrentTokenAbsolutePosition(); + t.absoluteEnd = t.absoluteBegin + t.image.length(); + positionComments(t); + lastToken = t; + // Track the lexical region independently of parser lookahead: nested SELECT + // disambiguation can fetch PATTERN tokens before its production is entered. + if (rowPatternContext == null && t.kind == S_IDENTIFIER + && "MATCH_RECOGNIZE".equalsIgnoreCase(t.image) + && AbstractJSqlParser.Dialect.BIGQUERY.name().equals(configuration.getValue(Feature.dialect))) { + rowPatternContext = new RowPatternTokenContext(); + } + if (rowPatternContext != null) { + rowPatternContext.accept(t); + if (rowPatternContext.isIdle()) { rowPatternContext = null; } + } + } + + // the last token handed out with a token action, whose successor is the first end-of-file token + private Token lastToken; + + /** + * Gives the comments before the end of input their positions. The end-of-file token gets no + * token action, and it is the first one after the last token that does which holds them. + */ + public void positionTrailingComments() { + if (lastToken != null && lastToken.next != null && lastToken.next.kind == EOF) { + positionComments(lastToken.next); + } + } + + /** Gives the comments in front of a token their positions. */ + public void positionComments(Token t) { + if (input_stream instanceof SimpleCharStream) { + ((SimpleCharStream) input_stream).positionSpecialTokens(t); + } + } + + public int getCurrentTokenAbsolutePosition() + { + if (input_stream instanceof SimpleCharStream) + return ((SimpleCharStream)input_stream).getAbsoluteTokenBegin(); + return -1; + } + + /** Scans a literal in linear time without tokenizing or rebuilding its whitespace. */ + public void consumeDollarQuotedString(String closingQuote) { + int[] prefix = new int[closingQuote.length()]; + for (int i = 1, matched = 0; i < closingQuote.length(); i++) { + while (matched > 0 && closingQuote.charAt(i) != closingQuote.charAt(matched)) { + matched = prefix[matched - 1]; + } + if (closingQuote.charAt(i) == closingQuote.charAt(matched)) { + matched++; + } + prefix[i] = matched; + } + try { + int matched = 0; + while (matched < closingQuote.length()) { + char ch = input_stream.readChar(); + while (matched > 0 && ch != closingQuote.charAt(matched)) { + matched = prefix[matched - 1]; + } + if (ch == closingQuote.charAt(matched)) { + matched++; + } + } + } catch (java.io.IOException e) { + reportError(Math.max(closingQuote.length(), input_stream.GetImage().length())); + } + } + + /** Rewinds any identifier suffix consumed by longest-match lexing before scanning the body. */ + private void consumeDollarQuotedToken(Token token, String delimiter) { + input_stream.backup(token.image.length() - delimiter.length()); + consumeDollarQuotedString(delimiter); + token.image = input_stream.GetImage(); + token.kind = charLiteralIndex; + token.endLine = input_stream.getEndLine(); + token.endColumn = input_stream.getEndColumn(); + } + + /** + * DuckDB's and PostgreSQL's digit separators, as in {@code 1_000_000}. Under + * Feature.allowDigitSeparators they are dropped from the image. Otherwise the token is + * lexed as without separators: the longer of the number before the first {@code _} and the + * identifier starting there, so that MySQL's {@code 2024_01} stays an identifier. + */ + private void lexDigitSeparators(Token token) { + int separator = token.image.indexOf('_'); + if (separator < 0) { + return; + } + if (input_stream.featureConfiguration != null + && Boolean.TRUE.equals(configuration.getValue(Feature.allowDigitSeparators))) { + token.image = token.image.replace("_", ""); + return; + } + int identifierLength = 0; + while (identifierLength < token.image.length() + && (Character.isLetterOrDigit(token.image.charAt(identifierLength)) + || token.image.charAt(identifierLength) == '_')) { + identifierLength++; + } + if (identifierLength > separator) { + truncateIdentifierToken(token, identifierLength); + token.kind = S_IDENTIFIER; + } else { + truncateIdentifierToken(token, separator); + token.kind = token.image.indexOf('.') >= 0 || token.image.indexOf('e') >= 0 + || token.image.indexOf('E') >= 0 ? S_DOUBLE : S_LONG; + } + } + + /** Splits a longest-match identifier without losing token positions. */ + private void truncateIdentifierToken(Token token, int length) { + input_stream.backup(token.image.length() - length); + token.image = token.image.substring(0, length); + token.endLine = input_stream.getEndLine(); + token.endColumn = input_stream.getEndColumn(); + } + + /** + * Consumes the body of a block comment after the opening delimiter has been matched, + * honouring nesting, up to and including the outermost closing delimiter. Then backs + * up 2 characters so the MULTI_LINE_COMMENT rule matches the closing delimiter and + * terminates the MORE chain. + */ + void consumeBlockCommentBody() { + int nesting = 1; + char prev = 0; + try { + while (nesting > 0) { + char c = input_stream.readChar(); + if (prev == '/' && c == '*') { + nesting++; + prev = 0; + } else if (prev == '*' && c == '/') { + nesting--; + prev = 0; + } else { + prev = c; + } + } + input_stream.backup(2); + } catch (java.io.IOException e) { + // Unterminated comment: leave the stream at EOF and let the token + // manager raise the lexical error from IN_BLOCK_COMMENT. + } + } +} + +SKIP: +{ + +} + +// Sentinel: start of non-reserved keyword range (for O(1) identifier checks) +SKIP: +{ + +} + +/** + * Non-reserved SQL keywords usable as unquoted identifiers. + * Tokens are declared inline to get consecutive kind values between + * MIN_NON_RESERVED_WORD and MAX_NON_RESERVED_WORD sentinels. + * + * The words are split over two productions: the lookahead routine JavaCC generates for one + * choice of all of them exceeds the JVM's 8000 byte limit for JIT compiled methods + * (-XX:HugeMethodLimit) and would always run interpreted. + */ +String NonReservedWord() : +{ String word; } +{ + ( word=NonReservedWordPart1() | word=NonReservedWordPart2() ) + { return word; } +} + +String NonReservedWordPart1() : +{ Token tk = null; } +{ + ( + tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= | tk= | tk= | tk= @@ -1104,16 +4034,20 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1121,6 +4055,8 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= @@ -1128,6 +4064,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1135,9 +4072,11 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1156,11 +4095,15 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= @@ -1170,36 +4113,49 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1221,7 +4177,10 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= @@ -1231,43 +4190,59 @@ String NonReservedWord() : | tk= | tk= | tk= - | tk= + ) + { return tk.image; } +} + +String NonReservedWordPart2() : +{ Token tk = null; } +{ + ( + tk= | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1284,21 +4259,29 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= + | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1307,17 +4290,22 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1332,8 +4320,11 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= + | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1342,21 +4333,32 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= + | tk= + | tk= + | tk= | tk= + | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= @@ -1383,6 +4385,8 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= @@ -1395,6 +4399,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1410,11767 +4415,20630 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= ) - { return tk.image; } + { return tk.image; } +} + +// Sentinel: end of non-reserved keyword range +SKIP: +{ + +} + +TOKEN: /* Reserved SQL Keywords and structural tokens */ +{ + +| +| +| +| +| +| +| +| /* H2 casewhen function */ +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /* Salesforce SOQL */ +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /* Salesforce SOQL */ +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| "> +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| | +| +| +| +| (" ")+ ) > +} + +TOKEN : /* Statement Separators */ +{ + +} + +TOKEN : /* Operators */ +{ + " ()* "="> +| "> +| )* "="> +| )* ">"> +| )* "="> +| +| +| +| +| +| +| >"> +| +| +| +| { + if (!input_stream.mySqlMinusSubstitution) { + input_stream.mySqlCommentError = "Unexpected control character"; + reportMySqlCommentError(); + } + matchedToken.kind = OP_MINUS; + matchedToken.image = "-"; + } +| { + reportMySqlCommentError(); + } +| +| "> +| +} + +TOKEN : /* Date/Time with time zones */ +{ + ()* ("(" ")")? ()* ( | ) (()+ )? ()+ "TIME" ()+ > +} + +TOKEN : /* Data Types */ +{ + // Dedicated keyword tokens (e.g. K_BINARY/K_CHAR) are consumed by DataType(). + // Repeating them here cannot match DATA_TYPE and causes unreachable-token warnings. + | | | + | | | | | | + | | | | | + | | | | + | | | + ) > + + | <#TYPE_BIT: "BISTRING"> + | <#TYPE_BLOB: "BLOB" | "BYTEA" | "VARBINARY" > + | <#TYPE_BOOLEAN: "BOOL" > + | <#TYPE_CLOB: "CLOB"> + | <#TYPE_ENUM: "ENUM" > + | <#TYPE_MAP: "MAP" > + | <#TYPE_DECIMAL: "DECIMAL" | "NUMBER" | "NUMERIC" > + | <#TYPE_TINYINT: "TINYINT" | "INT1" > + | <#TYPE_SMALLINT: "SMALLINT" | "INT2" | "SHORT" > + | <#TYPE_INTEGER: ( "INTEGER" | "INT" | "INT4" ) > + | <#TYPE_BIGINT: "BIGINT" | "INT8" | "LONG" > + | <#TYPE_HUGEINT: "HUGEINT" > + | <#TYPE_UTINYINT: "UTINYINT" > + | <#TYPE_USMALLINT: "USMALLINT" > + | <#TYPE_UINTEGER: "UINTEGER" > + | <#TYPE_UBIGINT: "UBIGINT" > + | <#TYPE_UHUGEINT: "UHUGEINT" > + | <#TYPE_REAL: "REAL" | "FLOAT4" | "FLOAT"> + | <#TYPE_DOUBLE: "DOUBLE" | "PRECISION" | "FLOAT8" | "FLOAT64"> + | <#TYPE_VARCHAR: "NVARCHAR" | "VARCHAR" | "NCHAR" | "BPCHAR" | "TEXT" | "STRING" | "VARYING"> + | <#TYPE_TIME: "TIMETZ" > + | <#TYPE_TIMESTAMP: "TIMESTAMP_NS" | "TIMESTAMP_MS" | "TIMESTAMP_S" > + + | <#TYPE_UUID: "UUID"> +} + +TOKEN : /* Numeric Constants */ +{ + < S_DOUBLE: (()? "." ( ["e","E"] (["+", "-"])? )? + | + "." (["e","E"] (["+", "-"])? )? + | + ["e","E"] (["+", "-"])? + )> { lexDigitSeparators(matchedToken); } + | < S_LONG: ( )+ ( "_" ( )+ )* > { lexDigitSeparators(matchedToken); } + | < #DIGIT: ["0" - "9"] > + | < S_HEX: ("X" ( (" ")* )* | "0x" ( )+ ) > + | < #QUOTED_HEX: "'" ( )* "'" > + | < #HEX_VALUE: ["0"-"9","A"-"F", " "] > + | < #HEX_DIGIT: ["0"-"9","A"-"F"] > +} + +SPECIAL_TOKEN: +{ + < LINE_COMMENT: ("--" | "//") (~["\r","\n"])*> +} + +// MySQL `#` line comment (#2499), flag-gated. Under +// Feature.allowHashLineComments the stream rewrites a token-start `#` to +// SimpleCharStream.HASH_SUBSTITUTION, a character no other rule starts with, +// so this production wins the match for every `#` form (`# c`, `#c`, `#>`, +// ...) without touching the identifier and JSON-operator lexing of the +// default mode. The action restores the `#` in the token image; unquoted +// identifiers end at their first `#` via the S_IDENTIFIER action, which +// re-lexes the remainder as this comment. +SPECIAL_TOKEN: +{ + < HASH_LINE_COMMENT: "\u0001" (~["\r","\n"])*> +} + +// Nested block comments: /* ... /* ... */ ... */ +// +// The body is consumed manually in the MORE action below because JavaCC 8 +// discards actions attached to SPECIAL_TOKEN and SKIP rules: it emits +// SkipLexicalActions() with an empty switch. Only TOKEN and MORE actions +// survive code generation. Do not move this action onto the rule below. +// +// consumeBlockCommentBody() scans past the whole comment, honouring nesting, +// then backs up 2 chars so re-matches the outermost "*/" +// and closes the MORE chain. No character past the comment is ever consumed, +// so there is nothing to push back and no dependency on what follows. +MORE: +{ + "/*" { consumeBlockCommentBody(); } : IN_BLOCK_COMMENT +} + + SPECIAL_TOKEN: +{ + : DEFAULT +} + +TOKEN: +{ + +| + + { + if (rowPatternContext != null && rowPatternContext.isInPattern()) { + truncateIdentifierToken(matchedToken, 1); + matchedToken.kind = S_IDENTIFIER; + } else { + consumeDollarQuotedToken(matchedToken, matchedToken.image); + } + } +| +// Bare `#` as a binary operator (PostgreSQL bitwise XOR / geometric +// intersection). Declared before to win the length tie on a +// lone `#`; `#temp`-style identifiers and `#>` / `#>>` keep their usual +// lexing by longest match (#1197, #1695). + +| + ()*) | "$" | ("$" ()*)> +{ + if (rowPatternContext != null && rowPatternContext.isInPattern()) { + int dollar = matchedToken.image.indexOf('$'); + if (dollar >= 0) { truncateIdentifierToken(matchedToken, Math.max(1, dollar)); } + } + if (matchedToken.image.charAt(0) == '$' + && Boolean.TRUE.equals(configuration.getValue(Feature.allowDollarQuotedStringTags))) { + String delimiter = StringValue.getDollarQuoteDelimiter(matchedToken.image); + if (delimiter != null) { + consumeDollarQuotedToken(matchedToken, delimiter); + } + } + // MySQL `#` line comments (#2499): under the flag an unquoted identifier + // ends at its first `#`, the rest of the line becomes a comment via the + // stream-level substitution (real MySQL reads `42#24` as `42` plus + // comment too). Quoted identifiers and strings keep their `#`. + // the wiring check keeps the flag lookup off the hot path of parses + // that never opted in (the stream is only wired through the feature + // consumers / withConfiguration); getValue avoids the String-based + // getAsBoolean roundtrip + if (matchedToken.kind == S_IDENTIFIER && input_stream.featureConfiguration != null + && Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) { + int hashIndex = matchedToken.image.indexOf('#'); + if (hashIndex > 0) { + truncateIdentifierToken(matchedToken, hashIndex); + } + } + // PostgreSQL does not allow # or @ in unquoted identifiers. Re-lex them as + // operators, including #>, #>>, #- and @?, when touching the left operand. + if (matchedToken.kind == S_IDENTIFIER + && AbstractJSqlParser.Dialect.POSTGRESQL.name().equals(configuration.getValue(Feature.dialect))) { + int hashIndex = matchedToken.image.indexOf('#'); + int atIndex = matchedToken.image.indexOf('@'); + int operatorIndex = hashIndex < 0 ? atIndex + : atIndex < 0 ? hashIndex : Math.min(hashIndex, atIndex); + if (operatorIndex > 0) { + truncateIdentifierToken(matchedToken, operatorIndex); + } else if (hashIndex == 0) { + truncateIdentifierToken(matchedToken, 1); + matchedToken.kind = S_HASH_OPERATOR; + } + } +} +| <#LETTER: + | | [ "#", "_" ] // Not SQL:2016 compliant! + > +| <#PART_LETTER_NO_DOLLAR: | | [ "#", "_" , "@" ] > +| <#PART_LETTER: | | [ "$" , "#", "_" , "@" ] > +| ()? > +{ + // same truncation as S_IDENTIFIER: `@@#name` ends at the `#` under + // allowHashLineComments, the rest of the line becomes the comment + // the wiring check keeps the flag lookup off the hot path of parses + // that never opted in (the stream is only wired through the feature + // consumers / withConfiguration); getValue avoids the String-based + // getAsBoolean roundtrip + if (input_stream.featureConfiguration != null + && Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) { + int hashIndex = matchedToken.image.indexOf('#'); + if (hashIndex > 0) { + truncateIdentifierToken(matchedToken, hashIndex); + } + } +} + +// Unicode characters and categories are defined here: https://www.unicode.org/Public/UNIDATA/UnicodeData.txt +// SQL:2016 states: +// An is any character in the Unicode General Category classes “Lu”, “Ll”, “Lt”, “Lm”, “Lo”, or “Nl”. +// An is U+00B7, “Middle Dot”, or any character in the Unicode General Category classes “Mn”, “Mc”, “Nd”, “Pc”, or “Cf”. + +// unicode_identifier_start +| <#UnicodeIdentifierStart: ("\u00B7" | | | | | | |) > +| <#Ll: ["a"-"z","µ","ß"-"ö","ø"-"ÿ","ā","ă","ą","ć","ĉ","ċ","č","ď","đ","ē","ĕ","ė","ę","ě","ĝ","ğ","ġ","ģ","ĥ","ħ","ĩ","ī","ĭ","į","ı","ij","ĵ","ķ"-"ĸ","ĺ","ļ","ľ","ŀ","ł","ń","ņ","ň"-"ʼn","ŋ","ō","ŏ","ő","œ","ŕ","ŗ","ř","ś","ŝ","ş","š","ţ","ť","ŧ","ũ","ū","ŭ","ů","ű","ų","ŵ","ŷ","ź","ż","ž"-"ƀ","ƃ","ƅ","ƈ","ƌ"-"ƍ","ƒ","ƕ","ƙ"-"ƛ","ƞ","ơ","ƣ","ƥ","ƨ","ƪ"-"ƫ","ƭ","ư","ƴ","ƶ","ƹ"-"ƺ","ƽ"-"ƿ","dž","lj","nj","ǎ","ǐ","ǒ","ǔ","ǖ","ǘ","ǚ","ǜ"-"ǝ","ǟ","ǡ","ǣ","ǥ","ǧ","ǩ","ǫ","ǭ","ǯ"-"ǰ","dz","ǵ","ǹ","ǻ","ǽ","ǿ","ȁ","ȃ","ȅ","ȇ","ȉ","ȋ","ȍ","ȏ","ȑ","ȓ","ȕ","ȗ","ș","ț","ȝ","ȟ","ȡ","ȣ","ȥ","ȧ","ȩ","ȫ","ȭ","ȯ","ȱ","ȳ"-"ȹ","ȼ","ȿ"-"ɀ","ɂ","ɇ","ɉ","ɋ","ɍ","ɏ"-"ʓ","ʕ"-"ʯ","ͱ","ͳ","ͷ","ͻ"-"ͽ","ΐ","ά"-"ώ","ϐ"-"ϑ","ϕ"-"ϗ","ϙ","ϛ","ϝ","ϟ","ϡ","ϣ","ϥ","ϧ","ϩ","ϫ","ϭ","ϯ"-"ϳ","ϵ","ϸ","ϻ"-"ϼ","а"-"џ","ѡ","ѣ","ѥ","ѧ","ѩ","ѫ","ѭ","ѯ","ѱ","ѳ","ѵ","ѷ","ѹ","ѻ","ѽ","ѿ","ҁ","ҋ","ҍ","ҏ","ґ","ғ","ҕ","җ","ҙ","қ","ҝ","ҟ","ҡ","ң","ҥ","ҧ","ҩ","ҫ","ҭ","ү","ұ","ҳ","ҵ","ҷ","ҹ","һ","ҽ","ҿ","ӂ","ӄ","ӆ","ӈ","ӊ","ӌ","ӎ"-"ӏ","ӑ","ӓ","ӕ","ӗ","ә","ӛ","ӝ","ӟ","ӡ","ӣ","ӥ","ӧ","ө","ӫ","ӭ","ӯ","ӱ","ӳ","ӵ","ӷ","ӹ","ӻ","ӽ","ӿ","ԁ","ԃ","ԅ","ԇ","ԉ","ԋ","ԍ","ԏ","ԑ","ԓ","ԕ","ԗ","ԙ","ԛ","ԝ","ԟ","ԡ","ԣ","ԥ","ԧ","ԩ","ԫ","ԭ","ԯ","ՠ"-"ֈ","ა"-"ჺ","ჽ"-"ჿ","ᏸ"-"ᏽ","ᲀ"-"ᲈ","ᴀ"-"ᴫ","ᵫ"-"ᵷ","ᵹ"-"ᶚ","ḁ","ḃ","ḅ","ḇ","ḉ","ḋ","ḍ","ḏ","ḑ","ḓ","ḕ","ḗ","ḙ","ḛ","ḝ","ḟ","ḡ","ḣ","ḥ","ḧ","ḩ","ḫ","ḭ","ḯ","ḱ","ḳ","ḵ","ḷ","ḹ","ḻ","ḽ","ḿ","ṁ","ṃ","ṅ","ṇ","ṉ","ṋ","ṍ","ṏ","ṑ","ṓ","ṕ","ṗ","ṙ","ṛ","ṝ","ṟ","ṡ","ṣ","ṥ","ṧ","ṩ","ṫ","ṭ","ṯ","ṱ","ṳ","ṵ","ṷ","ṹ","ṻ","ṽ","ṿ","ẁ","ẃ","ẅ","ẇ","ẉ","ẋ","ẍ","ẏ","ẑ","ẓ","ẕ"-"ẝ","ẟ","ạ","ả","ấ","ầ","ẩ","ẫ","ậ","ắ","ằ","ẳ","ẵ","ặ","ẹ","ẻ","ẽ","ế","ề","ể","ễ","ệ","ỉ","ị","ọ","ỏ","ố","ồ","ổ","ỗ","ộ","ớ","ờ","ở","ỡ","ợ","ụ","ủ","ứ","ừ","ử","ữ","ự","ỳ","ỵ","ỷ","ỹ","ỻ","ỽ","ỿ"-"ἇ","ἐ"-"ἕ","ἠ"-"ἧ","ἰ"-"ἷ","ὀ"-"ὅ","ὐ"-"ὗ","ὠ"-"ὧ","ὰ"-"ώ","ᾀ"-"ᾇ","ᾐ"-"ᾗ","ᾠ"-"ᾧ","ᾰ"-"ᾴ","ᾶ"-"ᾷ","ι","ῂ"-"ῄ","ῆ"-"ῇ","ῐ"-"ΐ","ῖ"-"ῗ","ῠ"-"ῧ","ῲ"-"ῴ","ῶ"-"ῷ","ℊ","ℎ"-"ℏ","ℓ","ℯ","ℴ","ℹ","ℼ"-"ℽ","ⅆ"-"ⅉ","ⅎ","ↄ","ⰰ"-"ⱟ","ⱡ","ⱥ"-"ⱦ","ⱨ","ⱪ","ⱬ","ⱱ","ⱳ"-"ⱴ","ⱶ"-"ⱻ","ⲁ","ⲃ","ⲅ","ⲇ","ⲉ","ⲋ","ⲍ","ⲏ","ⲑ","ⲓ","ⲕ","ⲗ","ⲙ","ⲛ","ⲝ","ⲟ","ⲡ","ⲣ","ⲥ","ⲧ","ⲩ","ⲫ","ⲭ","ⲯ","ⲱ","ⲳ","ⲵ","ⲷ","ⲹ","ⲻ","ⲽ","ⲿ","ⳁ","ⳃ","ⳅ","ⳇ","ⳉ","ⳋ","ⳍ","ⳏ","ⳑ","ⳓ","ⳕ","ⳗ","ⳙ","ⳛ","ⳝ","ⳟ","ⳡ","ⳣ"-"ⳤ","ⳬ","ⳮ","ⳳ","ⴀ"-"ⴥ","ⴧ","ⴭ","ꙁ","ꙃ","ꙅ","ꙇ","ꙉ","ꙋ","ꙍ","ꙏ","ꙑ","ꙓ","ꙕ","ꙗ","ꙙ","ꙛ","ꙝ","ꙟ","ꙡ","ꙣ","ꙥ","ꙧ","ꙩ","ꙫ","ꙭ","ꚁ","ꚃ","ꚅ","ꚇ","ꚉ","ꚋ","ꚍ","ꚏ","ꚑ","ꚓ","ꚕ","ꚗ","ꚙ","ꚛ","ꜣ","ꜥ","ꜧ","ꜩ","ꜫ","ꜭ","ꜯ"-"ꜱ","ꜳ","ꜵ","ꜷ","ꜹ","ꜻ","ꜽ","ꜿ","ꝁ","ꝃ","ꝅ","ꝇ","ꝉ","ꝋ","ꝍ","ꝏ","ꝑ","ꝓ","ꝕ","ꝗ","ꝙ","ꝛ","ꝝ","ꝟ","ꝡ","ꝣ","ꝥ","ꝧ","ꝩ","ꝫ","ꝭ","ꝯ","ꝱ"-"ꝸ","ꝺ","ꝼ","ꝿ","ꞁ","ꞃ","ꞅ","ꞇ","ꞌ","ꞎ","ꞑ","ꞓ"-"ꞕ","ꞗ","ꞙ","ꞛ","ꞝ","ꞟ","ꞡ","ꞣ","ꞥ","ꞧ","ꞩ","ꞯ","ꞵ","ꞷ","ꞹ","ꞻ","ꞽ","ꞿ","ꟁ","ꟃ","ꟈ","ꟊ","ꟑ","ꟓ","ꟕ","ꟗ","ꟙ","ꟶ","ꟺ","ꬰ"-"ꭚ","ꭠ"-"ꭨ","ꭰ"-"ꮿ","ff"-"st","ﬓ"-"ﬗ","a"-"z"]> +| <#Lm: ["ʰ"-"ˁ","ˆ"-"ˑ","ˠ"-"ˤ","ˬ","ˮ","ʹ","ͺ","ՙ","ـ","ۥ"-"ۦ","ߴ"-"ߵ","ߺ","ࠚ","ࠤ","ࠨ","ࣉ","ॱ","ๆ","ໆ","ჼ","ៗ","ᡃ","ᪧ","ᱸ"-"ᱽ","ᴬ"-"ᵪ","ᵸ","ᶛ"-"ᶿ","ⁱ","ⁿ","ₐ"-"ₜ","ⱼ"-"ⱽ","ⵯ","ⸯ","々","〱"-"〵","〻","ゝ"-"ゞ","ー"-"ヾ","ꀕ","ꓸ"-"ꓽ","ꘌ","ꙿ","ꚜ"-"ꚝ","ꜗ"-"ꜟ","ꝰ","ꞈ","ꟲ"-"ꟴ","ꟸ"-"ꟹ","ꧏ","ꧦ","ꩰ","ꫝ","ꫳ"-"ꫴ","ꭜ"-"ꭟ","ꭩ","ー","゙"-"゚"]> +| <#Lo: ["ª","º","ƻ","ǀ"-"ǃ","ʔ","א"-"ת","ׯ"-"ײ","ؠ"-"ؿ","ف"-"ي","ٮ"-"ٯ","ٱ"-"ۓ","ە","ۮ"-"ۯ","ۺ"-"ۼ","ۿ","ܐ","ܒ"-"ܯ","ݍ"-"ޥ","ޱ","ߊ"-"ߪ","ࠀ"-"ࠕ","ࡀ"-"ࡘ","ࡠ"-"ࡪ","ࡰ"-"ࢇ","ࢉ"-"ࢎ","ࢠ"-"ࣈ","ऄ"-"ह","ऽ","ॐ","क़"-"ॡ","ॲ"-"ঀ","অ"-"ঌ","এ"-"ঐ","ও"-"ন","প"-"র","ল","শ"-"হ","ঽ","ৎ","ড়"-"ঢ়","য়"-"ৡ","ৰ"-"ৱ","ৼ","ਅ"-"ਊ","ਏ"-"ਐ","ਓ"-"ਨ","ਪ"-"ਰ","ਲ"-"ਲ਼","ਵ"-"ਸ਼","ਸ"-"ਹ","ਖ਼"-"ੜ","ਫ਼","ੲ"-"ੴ","અ"-"ઍ","એ"-"ઑ","ઓ"-"ન","પ"-"ર","લ"-"ળ","વ"-"હ","ઽ","ૐ","ૠ"-"ૡ","ૹ","ଅ"-"ଌ","ଏ"-"ଐ","ଓ"-"ନ","ପ"-"ର","ଲ"-"ଳ","ଵ"-"ହ","ଽ","ଡ଼"-"ଢ଼","ୟ"-"ୡ","ୱ","ஃ","அ"-"ஊ","எ"-"ஐ","ஒ"-"க","ங"-"ச","ஜ","ஞ"-"ட","ண"-"த","ந"-"ப","ம"-"ஹ","ௐ","అ"-"ఌ","ఎ"-"ఐ","ఒ"-"న","ప"-"హ","ఽ","ౘ"-"ౚ","ౝ","ౠ"-"ౡ","ಀ","ಅ"-"ಌ","ಎ"-"ಐ","ಒ"-"ನ","ಪ"-"ಳ","ವ"-"ಹ","ಽ","ೝ"-"ೞ","ೠ"-"ೡ","ೱ"-"ೲ","ഄ"-"ഌ","എ"-"ഐ","ഒ"-"ഺ","ഽ","ൎ","ൔ"-"ൖ","ൟ"-"ൡ","ൺ"-"ൿ","අ"-"ඖ","ක"-"න","ඳ"-"ර","ල","ව"-"ෆ","ก"-"ะ","า"-"ำ","เ"-"ๅ","ກ"-"ຂ","ຄ","ຆ"-"ຊ","ຌ"-"ຣ","ລ","ວ"-"ະ","າ"-"ຳ","ຽ","ເ"-"ໄ","ໜ"-"ໟ","ༀ","ཀ"-"ཇ","ཉ"-"ཬ","ྈ"-"ྌ","က"-"ဪ","ဿ","ၐ"-"ၕ","ၚ"-"ၝ","ၡ","ၥ"-"ၦ","ၮ"-"ၰ","ၵ"-"ႁ","ႎ","ᄀ"-"ቈ","ቊ"-"ቍ","ቐ"-"ቖ","ቘ","ቚ"-"ቝ","በ"-"ኈ","ኊ"-"ኍ","ነ"-"ኰ","ኲ"-"ኵ","ኸ"-"ኾ","ዀ","ዂ"-"ዅ","ወ"-"ዖ","ዘ"-"ጐ","ጒ"-"ጕ","ጘ"-"ፚ","ᎀ"-"ᎏ","ᐁ"-"ᙬ","ᙯ"-"ᙿ","ᚁ"-"ᚚ","ᚠ"-"ᛪ","ᛱ"-"ᛸ","ᜀ"-"ᜑ","ᜟ"-"ᜱ","ᝀ"-"ᝑ","ᝠ"-"ᝬ","ᝮ"-"ᝰ","ក"-"ឳ","ៜ","ᠠ"-"ᡂ","ᡄ"-"ᡸ","ᢀ"-"ᢄ","ᢇ"-"ᢨ","ᢪ","ᢰ"-"ᣵ","ᤀ"-"ᤞ","ᥐ"-"ᥭ","ᥰ"-"ᥴ","ᦀ"-"ᦫ","ᦰ"-"ᧉ","ᨀ"-"ᨖ","ᨠ"-"ᩔ","ᬅ"-"ᬳ","ᭅ"-"ᭌ","ᮃ"-"ᮠ","ᮮ"-"ᮯ","ᮺ"-"ᯥ","ᰀ"-"ᰣ","ᱍ"-"ᱏ","ᱚ"-"ᱷ","ᳩ"-"ᳬ","ᳮ"-"ᳳ","ᳵ"-"ᳶ","ᳺ","ℵ"-"ℸ","ⴰ"-"ⵧ","ⶀ"-"ⶖ","ⶠ"-"ⶦ","ⶨ"-"ⶮ","ⶰ"-"ⶶ","ⶸ"-"ⶾ","ⷀ"-"ⷆ","ⷈ"-"ⷎ","ⷐ"-"ⷖ","ⷘ"-"ⷞ","〆","〼","ぁ"-"ゖ","ゟ","ァ"-"ヺ","ヿ","ㄅ"-"ㄯ","ㄱ"-"ㆎ","ㆠ"-"ㆿ","ㇰ"-"ㇿ","䶿","鿿"-"ꀔ","ꀖ"-"ꒌ","ꓐ"-"ꓷ","ꔀ"-"ꘋ","ꘐ"-"ꘟ","ꘪ"-"ꘫ","ꙮ","ꚠ"-"ꛥ","ꞏ","ꟷ","ꟻ"-"ꠁ","ꠃ"-"ꠅ","ꠇ"-"ꠊ","ꠌ"-"ꠢ","ꡀ"-"ꡳ","ꢂ"-"ꢳ","ꣲ"-"ꣷ","ꣻ","ꣽ"-"ꣾ","ꤊ"-"ꤥ","ꤰ"-"ꥆ","ꥠ"-"ꥼ","ꦄ"-"ꦲ","ꧠ"-"ꧤ","ꧧ"-"ꧯ","ꧺ"-"ꧾ","ꨀ"-"ꨨ","ꩀ"-"ꩂ","ꩄ"-"ꩋ","ꩠ"-"ꩯ","ꩱ"-"ꩶ","ꩺ","ꩾ"-"ꪯ","ꪱ","ꪵ"-"ꪶ","ꪹ"-"ꪽ","ꫀ","ꫂ","ꫛ"-"ꫜ","ꫠ"-"ꫪ","ꫲ","ꬁ"-"ꬆ","ꬉ"-"ꬎ","ꬑ"-"ꬖ","ꬠ"-"ꬦ","ꬨ"-"ꬮ","ꯀ"-"ꯢ","힣","ힰ"-"ퟆ","ퟋ"-"ퟻ","豈"-"舘","並"-"龎","יִ","ײַ"-"ﬨ","שׁ"-"זּ","טּ"-"לּ","מּ","נּ"-"סּ","ףּ"-"פּ","צּ"-"ﮱ","ﯓ"-"ﴽ","ﵐ"-"ﶏ","ﶒ"-"ﷇ","ﷰ"-"ﷻ","ﹰ"-"ﹴ","ﹶ"-"ﻼ","ヲ"-"ッ","ア"-"ン","ᅠ"-"ᄒ","ᅡ"-"ᅦ","ᅧ"-"ᅬ","ᅭ"-"ᅲ","ᅳ"-"ᅵ"]> +| <#Lt: ["Dž","Lj","Nj","Dz","ᾈ"-"ᾏ","ᾘ"-"ᾟ","ᾨ"-"ᾯ","ᾼ","ῌ","ῼ"]> +| <#Lu: ["A"-"Z","À"-"Ö","Ø"-"Þ","Ā","Ă","Ą","Ć","Ĉ","Ċ","Č","Ď","Đ","Ē","Ĕ","Ė","Ę","Ě","Ĝ","Ğ","Ġ","Ģ","Ĥ","Ħ","Ĩ","Ī","Ĭ","Į","İ","IJ","Ĵ","Ķ","Ĺ","Ļ","Ľ","Ŀ","Ł","Ń","Ņ","Ň","Ŋ","Ō","Ŏ","Ő","Œ","Ŕ","Ŗ","Ř","Ś","Ŝ","Ş","Š","Ţ","Ť","Ŧ","Ũ","Ū","Ŭ","Ů","Ű","Ų","Ŵ","Ŷ","Ÿ"-"Ź","Ż","Ž","Ɓ"-"Ƃ","Ƅ","Ɔ"-"Ƈ","Ɖ"-"Ƌ","Ǝ"-"Ƒ","Ɠ"-"Ɣ","Ɩ"-"Ƙ","Ɯ"-"Ɲ","Ɵ"-"Ơ","Ƣ","Ƥ","Ʀ"-"Ƨ","Ʃ","Ƭ","Ʈ"-"Ư","Ʊ"-"Ƴ","Ƶ","Ʒ"-"Ƹ","Ƽ","DŽ","LJ","NJ","Ǎ","Ǐ","Ǒ","Ǔ","Ǖ","Ǘ","Ǚ","Ǜ","Ǟ","Ǡ","Ǣ","Ǥ","Ǧ","Ǩ","Ǫ","Ǭ","Ǯ","DZ","Ǵ","Ƕ"-"Ǹ","Ǻ","Ǽ","Ǿ","Ȁ","Ȃ","Ȅ","Ȇ","Ȉ","Ȋ","Ȍ","Ȏ","Ȑ","Ȓ","Ȕ","Ȗ","Ș","Ț","Ȝ","Ȟ","Ƞ","Ȣ","Ȥ","Ȧ","Ȩ","Ȫ","Ȭ","Ȯ","Ȱ","Ȳ","Ⱥ"-"Ȼ","Ƚ"-"Ⱦ","Ɂ","Ƀ"-"Ɇ","Ɉ","Ɋ","Ɍ","Ɏ","Ͱ","Ͳ","Ͷ","Ϳ","Ά","Έ"-"Ί","Ό","Ύ"-"Ώ","Α"-"Ρ","Σ"-"Ϋ","Ϗ","ϒ"-"ϔ","Ϙ","Ϛ","Ϝ","Ϟ","Ϡ","Ϣ","Ϥ","Ϧ","Ϩ","Ϫ","Ϭ","Ϯ","ϴ","Ϸ","Ϲ"-"Ϻ","Ͻ"-"Я","Ѡ","Ѣ","Ѥ","Ѧ","Ѩ","Ѫ","Ѭ","Ѯ","Ѱ","Ѳ","Ѵ","Ѷ","Ѹ","Ѻ","Ѽ","Ѿ","Ҁ","Ҋ","Ҍ","Ҏ","Ґ","Ғ","Ҕ","Җ","Ҙ","Қ","Ҝ","Ҟ","Ҡ","Ң","Ҥ","Ҧ","Ҩ","Ҫ","Ҭ","Ү","Ұ","Ҳ","Ҵ","Ҷ","Ҹ","Һ","Ҽ","Ҿ","Ӏ"-"Ӂ","Ӄ","Ӆ","Ӈ","Ӊ","Ӌ","Ӎ","Ӑ","Ӓ","Ӕ","Ӗ","Ә","Ӛ","Ӝ","Ӟ","Ӡ","Ӣ","Ӥ","Ӧ","Ө","Ӫ","Ӭ","Ӯ","Ӱ","Ӳ","Ӵ","Ӷ","Ӹ","Ӻ","Ӽ","Ӿ","Ԁ","Ԃ","Ԅ","Ԇ","Ԉ","Ԋ","Ԍ","Ԏ","Ԑ","Ԓ","Ԕ","Ԗ","Ԙ","Ԛ","Ԝ","Ԟ","Ԡ","Ԣ","Ԥ","Ԧ","Ԩ","Ԫ","Ԭ","Ԯ","Ա"-"Ֆ","Ⴀ"-"Ⴥ","Ⴧ","Ⴭ","Ꭰ"-"Ᏽ","Ა"-"Ჺ","Ჽ"-"Ჿ","Ḁ","Ḃ","Ḅ","Ḇ","Ḉ","Ḋ","Ḍ","Ḏ","Ḑ","Ḓ","Ḕ","Ḗ","Ḙ","Ḛ","Ḝ","Ḟ","Ḡ","Ḣ","Ḥ","Ḧ","Ḩ","Ḫ","Ḭ","Ḯ","Ḱ","Ḳ","Ḵ","Ḷ","Ḹ","Ḻ","Ḽ","Ḿ","Ṁ","Ṃ","Ṅ","Ṇ","Ṉ","Ṋ","Ṍ","Ṏ","Ṑ","Ṓ","Ṕ","Ṗ","Ṙ","Ṛ","Ṝ","Ṟ","Ṡ","Ṣ","Ṥ","Ṧ","Ṩ","Ṫ","Ṭ","Ṯ","Ṱ","Ṳ","Ṵ","Ṷ","Ṹ","Ṻ","Ṽ","Ṿ","Ẁ","Ẃ","Ẅ","Ẇ","Ẉ","Ẋ","Ẍ","Ẏ","Ẑ","Ẓ","Ẕ","ẞ","Ạ","Ả","Ấ","Ầ","Ẩ","Ẫ","Ậ","Ắ","Ằ","Ẳ","Ẵ","Ặ","Ẹ","Ẻ","Ẽ","Ế","Ề","Ể","Ễ","Ệ","Ỉ","Ị","Ọ","Ỏ","Ố","Ồ","Ổ","Ỗ","Ộ","Ớ","Ờ","Ở","Ỡ","Ợ","Ụ","Ủ","Ứ","Ừ","Ử","Ữ","Ự","Ỳ","Ỵ","Ỷ","Ỹ","Ỻ","Ỽ","Ỿ","Ἀ"-"Ἇ","Ἐ"-"Ἕ","Ἠ"-"Ἧ","Ἰ"-"Ἷ","Ὀ"-"Ὅ","Ὑ","Ὓ","Ὕ","Ὗ","Ὠ"-"Ὧ","Ᾰ"-"Ά","Ὲ"-"Ή","Ῐ"-"Ί","Ῠ"-"Ῥ","Ὸ"-"Ώ","ℂ","ℇ","ℋ"-"ℍ","ℐ"-"ℒ","ℕ","ℙ"-"ℝ","ℤ","Ω","ℨ","K"-"ℭ","ℰ"-"ℳ","ℾ"-"ℿ","ⅅ","Ↄ","Ⰰ"-"Ⱟ","Ⱡ","Ɫ"-"Ɽ","Ⱨ","Ⱪ","Ⱬ","Ɑ"-"Ɒ","Ⱳ","Ⱶ","Ȿ"-"Ⲁ","Ⲃ","Ⲅ","Ⲇ","Ⲉ","Ⲋ","Ⲍ","Ⲏ","Ⲑ","Ⲓ","Ⲕ","Ⲗ","Ⲙ","Ⲛ","Ⲝ","Ⲟ","Ⲡ","Ⲣ","Ⲥ","Ⲧ","Ⲩ","Ⲫ","Ⲭ","Ⲯ","Ⲱ","Ⲳ","Ⲵ","Ⲷ","Ⲹ","Ⲻ","Ⲽ","Ⲿ","Ⳁ","Ⳃ","Ⳅ","Ⳇ","Ⳉ","Ⳋ","Ⳍ","Ⳏ","Ⳑ","Ⳓ","Ⳕ","Ⳗ","Ⳙ","Ⳛ","Ⳝ","Ⳟ","Ⳡ","Ⳣ","Ⳬ","Ⳮ","Ⳳ","Ꙁ","Ꙃ","Ꙅ","Ꙇ","Ꙉ","Ꙋ","Ꙍ","Ꙏ","Ꙑ","Ꙓ","Ꙕ","Ꙗ","Ꙙ","Ꙛ","Ꙝ","Ꙟ","Ꙡ","Ꙣ","Ꙥ","Ꙧ","Ꙩ","Ꙫ","Ꙭ","Ꚁ","Ꚃ","Ꚅ","Ꚇ","Ꚉ","Ꚋ","Ꚍ","Ꚏ","Ꚑ","Ꚓ","Ꚕ","Ꚗ","Ꚙ","Ꚛ","Ꜣ","Ꜥ","Ꜧ","Ꜩ","Ꜫ","Ꜭ","Ꜯ","Ꜳ","Ꜵ","Ꜷ","Ꜹ","Ꜻ","Ꜽ","Ꜿ","Ꝁ","Ꝃ","Ꝅ","Ꝇ","Ꝉ","Ꝋ","Ꝍ","Ꝏ","Ꝑ","Ꝓ","Ꝕ","Ꝗ","Ꝙ","Ꝛ","Ꝝ","Ꝟ","Ꝡ","Ꝣ","Ꝥ","Ꝧ","Ꝩ","Ꝫ","Ꝭ","Ꝯ","Ꝺ","Ꝼ","Ᵹ"-"Ꝿ","Ꞁ","Ꞃ","Ꞅ","Ꞇ","Ꞌ","Ɥ","Ꞑ","Ꞓ","Ꞗ","Ꞙ","Ꞛ","Ꞝ","Ꞟ","Ꞡ","Ꞣ","Ꞥ","Ꞧ","Ꞩ","Ɦ"-"Ɪ","Ʞ"-"Ꞵ","Ꞷ","Ꞹ","Ꞻ","Ꞽ","Ꞿ","Ꟁ","Ꟃ","Ꞔ"-"Ꟈ","Ꟊ","Ꟑ","Ꟗ","Ꟙ","Ꟶ","A"-"Z"]> +| <#Nl: ["ᛮ"-"ᛰ","Ⅰ"-"ↂ","ↅ"-"ↈ","〇","〡"-"〩","〸"-"〺","ꛦ"-"ꛯ"]> + +// unicode_identifier_extend +| <#UnicodeIdentifierExtend: (|||||)> +| <#Cf: ["­","؀"-"؅","؜","۝","܏","࢐"-"࢑","࣢","᠎","​"-"‏","‪"-"‮","⁠"-"⁤","⁦"-"","",""-""]> +| <#Mc: ["ः","ऻ","ा"-"ी","ॉ"-"ौ","ॎ"-"ॏ","ং"-"ঃ","া"-"ী","ে"-"ৈ","ো"-"ৌ","ৗ","ਃ","ਾ"-"ੀ","ઃ","ા"-"ી","ૉ","ો"-"ૌ","ଂ"-"ଃ","ା","ୀ","େ"-"ୈ","ୋ"-"ୌ","ୗ","ா"-"ி","ு"-"ூ","ெ"-"ை","ொ"-"ௌ","ௗ","ఁ"-"ః","ు"-"ౄ","ಂ"-"ಃ","ಾ","ೀ"-"ೄ","ೇ"-"ೈ","ೊ"-"ೋ","ೕ"-"ೖ","ೳ","ം"-"ഃ","ാ"-"ീ","െ"-"ൈ","ൊ"-"ൌ","ൗ","ං"-"ඃ","ා"-"ෑ","ෘ"-"ෟ","ෲ"-"ෳ","༾"-"༿","ཿ","ါ"-"ာ","ေ","း","ျ"-"ြ","ၖ"-"ၗ","ၢ"-"ၤ","ၧ"-"ၭ","ႃ"-"ႄ","ႇ"-"ႌ","ႏ","ႚ"-"ႜ","᜕","᜴","ា","ើ"-"ៅ","ះ"-"ៈ","ᤣ"-"ᤦ","ᤩ"-"ᤫ","ᤰ"-"ᤱ","ᤳ"-"ᤸ","ᨙ"-"ᨚ","ᩕ","ᩗ","ᩡ","ᩣ"-"ᩤ","ᩭ"-"ᩲ","ᬄ","ᬵ","ᬻ","ᬽ"-"ᭁ","ᭃ"-"᭄","ᮂ","ᮡ","ᮦ"-"ᮧ","᮪","ᯧ","ᯪ"-"ᯬ","ᯮ","᯲"-"᯳","ᰤ"-"ᰫ","ᰴ"-"ᰵ","᳡","᳷","〮"-"〯","ꠣ"-"ꠤ","ꠧ","ꢀ"-"ꢁ","ꢴ"-"ꣃ","ꥒ"-"꥓","ꦃ","ꦴ"-"ꦵ","ꦺ"-"ꦻ","ꦾ"-"꧀","ꨯ"-"ꨰ","ꨳ"-"ꨴ","ꩍ","ꩻ","ꩽ","ꫫ","ꫮ"-"ꫯ","ꫵ","ꯣ"-"ꯤ","ꯦ"-"ꯧ","ꯩ"-"ꯪ","꯬"]> +| <#Mn: ["̀"-"ͯ","҃"-"҇","֑"-"ֽ","ֿ","ׁ"-"ׂ","ׄ"-"ׅ","ׇ","ؐ"-"ؚ","ً"-"ٟ","ٰ","ۖ"-"ۜ","۟"-"ۤ","ۧ"-"ۨ","۪"-"ۭ","ܑ","ܰ"-"݊","ަ"-"ް","߫"-"߳","߽","ࠖ"-"࠙","ࠛ"-"ࠣ","ࠥ"-"ࠧ","ࠩ"-"࠭","࡙"-"࡛","࢘"-"࢟","࣊"-"࣡","ࣣ"-"ं","ऺ","़","ु"-"ै","्","॑"-"ॗ","ॢ"-"ॣ","ঁ","়","ু"-"ৄ","্","ৢ"-"ৣ","৾","ਁ"-"ਂ","਼","ੁ"-"ੂ","ੇ"-"ੈ","ੋ"-"੍","ੑ","ੰ"-"ੱ","ੵ","ઁ"-"ં","઼","ુ"-"ૅ","ે"-"ૈ","્","ૢ"-"ૣ","ૺ"-"૿","ଁ","଼","ି","ୁ"-"ୄ","୍","୕"-"ୖ","ୢ"-"ୣ","ஂ","ீ","்","ఀ","ఄ","఼","ా"-"ీ","ె"-"ై","ొ"-"్","ౕ"-"ౖ","ౢ"-"ౣ","ಁ","಼","ಿ","ೆ","ೌ"-"್","ೢ"-"ೣ","ഀ"-"ഁ","഻"-"഼","ു"-"ൄ","്","ൢ"-"ൣ","ඁ","්","ි"-"ු","ූ","ั","ิ"-"ฺ","็"-"๎","ັ","ິ"-"ຼ","່"-"໎","༘"-"༙","༵","༷","༹","ཱ"-"ཾ","ྀ"-"྄","྆"-"྇","ྍ"-"ྗ","ྙ"-"ྼ","࿆","ိ"-"ူ","ဲ"-"့","္"-"်","ွ"-"ှ","ၘ"-"ၙ","ၞ"-"ၠ","ၱ"-"ၴ","ႂ","ႅ"-"ႆ","ႍ","ႝ","፝"-"፟","ᜒ"-"᜔","ᜲ"-"ᜳ","ᝒ"-"ᝓ","ᝲ"-"ᝳ","឴"-"឵","ិ"-"ួ","ំ","៉"-"៓","៝","᠋"-"᠍","᠏","ᢅ"-"ᢆ","ᢩ","ᤠ"-"ᤢ","ᤧ"-"ᤨ","ᤲ","᤹"-"᤻","ᨗ"-"ᨘ","ᨛ","ᩖ","ᩘ"-"ᩞ","᩠","ᩢ","ᩥ"-"ᩬ","ᩳ"-"᩼","᩿","᪰"-"᪽","ᪿ"-"ᫎ","ᬀ"-"ᬃ","᬴","ᬶ"-"ᬺ","ᬼ","ᭂ","᭫"-"᭳","ᮀ"-"ᮁ","ᮢ"-"ᮥ","ᮨ"-"ᮩ","᮫"-"ᮭ","᯦","ᯨ"-"ᯩ","ᯭ","ᯯ"-"ᯱ","ᰬ"-"ᰳ","ᰶ"-"᰷","᳐"-"᳒","᳔"-"᳠","᳢"-"᳨","᳭","᳴","᳸"-"᳹","᷀"-"᷿","⃐"-"⃜","⃡","⃥"-"⃰","⳯"-"⳱","⵿","ⷠ"-"ⷿ","〪"-"〭","゙"-"゚","꙯","ꙴ"-"꙽","ꚞ"-"ꚟ","꛰"-"꛱","ꠂ","꠆","ꠋ","ꠥ"-"ꠦ","꠬","꣄"-"ꣅ","꣠"-"꣱","ꣿ","ꤦ"-"꤭","ꥇ"-"ꥑ","ꦀ"-"ꦂ","꦳","ꦶ"-"ꦹ","ꦼ"-"ꦽ","ꧥ","ꨩ"-"ꨮ","ꨱ"-"ꨲ","ꨵ"-"ꨶ","ꩃ","ꩌ","ꩼ","ꪰ","ꪲ"-"ꪴ","ꪷ"-"ꪸ","ꪾ"-"꪿","꫁","ꫬ"-"ꫭ","꫶","ꯥ","ꯨ","꯭","ﬞ","︀"-"️","︠"-"︯"]> +| <#Nd: ["0"-"9","٠"-"٩","۰"-"۹","߀"-"߉","०"-"९","০"-"৯","੦"-"੯","૦"-"૯","୦"-"୯","௦"-"௯","౦"-"౯","೦"-"೯","൦"-"൯","෦"-"෯","๐"-"๙","໐"-"໙","༠"-"༩","၀"-"၉","႐"-"႙","០"-"៩","᠐"-"᠙","᥆"-"᥏","᧐"-"᧙","᪀"-"᪉","᪐"-"᪙","᭐"-"᭙","᮰"-"᮹","᱀"-"᱉","᱐"-"᱙","꘠"-"꘩","꣐"-"꣙","꤀"-"꤉","꧐"-"꧙","꧰"-"꧹","꩐"-"꩙","꯰"-"꯹","0"-"9"]> +| <#Pc: ["‿"-"⁀","⁔","︳"-"︴","﹍"-"﹏","_"]> + +// CJK Unified Ideographs block according to https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) +| <#CJK: ["\uAC00"-"\uD7A3", "\u4E00"-"\u9FFF"]> + +| < S_CHAR_LITERAL: ( + (["U","E","N","R","B"]|"RB"|"_utf8")? + ( + // Backslash escapes and doubled quotes; trimmed below to the rules in effect + ("'" ( "\\" ~[] | "''" | ~["'", "\\"] )* "'") | ("'" ("''" | ~["'"])* "'") + // Alternative Oracle Escape Modes + | ("q'{" (~[])* "}'") + | ("q'(" (~[])* ")'") + | ("q'[" (~[])* "]'") + | ("q''" (~[])* "''") + // | ("q'\\" (~[])* "\\'") <--- Does not work + ) + // PostgreSQL Unicode escape string with an optional UESCAPE clause, see + // lexUnicodeEscapeString(). Backslashes do not escape its quotes. + | "U&'" ("''" | ~["'"])* "'" + ( ([" ", "\t", "\r", "\n"])* "UESCAPE" ([" ", "\t", "\r", "\n"])* "'" ~["'"] "'" )? + ) > + { + // The longest match may run past the closing quote of the active rules. A backslash + // escapes the next character only with backslash escapes or in a PostgreSQL E'' string; + // otherwise the literal ends at the first quote that is not doubled, so LIKE ... ESCAPE '\\' + // still works. + boolean backslashEscapes = configuration.getAsBoolean(Feature.allowBackslashEscapeCharacter) + || (AbstractJSqlParser.Dialect.POSTGRESQL.name().equals(configuration.getValue(Feature.dialect)) + && matchedToken.image.regionMatches(true, 0, "E'", 0, 2)); + int end = closingSingleQuote(matchedToken.image, backslashEscapes); + if (end > 0 && end < matchedToken.image.length()) { + matchedToken.image = image.substring(0, end); + input_stream.backup(image.length() - end); + } + if (isUnicodeEscapeString(matchedToken.image)) { + lexUnicodeEscapeString(matchedToken); + } + } +| < S_QUOTED_IDENTIFIER: "\"" ( "\"\"" | ~["\n","\r","\""])* "\"" + // MySQL double-quoted string with backslash escapes; trimmed below when not enabled + | "\"" ( "\"\"" | "\\" ~["\n","\r"] | ~["\n","\r","\"","\\"])* "\"" + | ("`" ("``" | ~["\n","\r","`"])* "`") | ( "[" (~["\n","\r","]"])* "]" ) > + { + if ( matchedToken.image.charAt(0) == '"' ) { + // Both alternatives above compete for the longest match. Keep the closing quote of + // the active quoting rules: a backslash escapes the next character only in + // double-quoted strings with backslash escapes, as in MySQL. + int end = closingQuote(matchedToken.image, 0, '"', + configuration.getAsBoolean(Feature.allowDoubleQuotedStrings) + && configuration.getAsBoolean(Feature.allowBackslashEscapeCharacter)); + if (end > 0 && end < matchedToken.image.length()) { + matchedToken.image = image.substring(0, end); + input_stream.backup(image.length() - end); + } + } + if ( !configuration.getAsBoolean(Feature.allowSquareBracketQuotation) + && matchedToken.image.charAt(0) == '[' ) { + + matchedToken.image = "["; + // `squaredBracketOpenIndex` defined in TokenManagerDeclaration above + matchedToken.kind = squaredBracketOpenIndex; + input_stream.backup(image.length() - 1); + } + if ( configuration.getAsBoolean(Feature.allowDoubleQuotedStrings) + && matchedToken.image.charAt(0) == '"' ) { + // `charLiteralIndex` defined in TokenManagerDeclaration above + matchedToken.kind = charLiteralIndex; + } + } +} + +/** + * Parses identifiers including standard SQL identifiers, quoted identifiers, and specific keywords. + * + * This is used in cases where certain SQL keywords (like NAME, NEXT, VALUE, etc.) can appear + * as identifiers (e.g., table names, column names) depending on the SQL dialect or context. + * + * Supported tokens: + * - : Standard unquoted SQL identifier + * - : Quoted identifier (e.g., `identifier` or "identifier") + * - , , , , , , : Specific keywords treated as identifiers + * + * @return Token representing the identifier or keyword used as identifier + */ +Token KeywordOrIdentifier(): +{ + Token tk; +} +{ + ( + tk = + | tk = + | tk = + | tk = + | tk = + | tk = + | tk = + | tk = + | tk = + | tk = + | tk = + ) + { return tk; } +} + +Statement Statement() #Statement: +{ + IfElseStatement ifElseStatement = null; + Statement stm = null; + Statement stm2 = null; + Expression condition; + Token ifFirst = null; +} +{ + ( + try { + ( + { ifFirst = getToken(0); } condition=Condition() + ( stm = SingleStatement() | stm = Block() ) { ifElseStatement = new IfElseStatement(condition, stm); linkAST(ifElseStatement, ifFirst); } + [ LOOKAHEAD(2) + [ { ifElseStatement.setUsingSemicolonForIfStatement(true); } ] + ( stm2 = SingleStatement() | stm2 = Block() ) { ifElseStatement.setElseStatement(stm2); widen(ifElseStatement, null, getToken(0)); } + ] + + [ + LOOKAHEAD(2) + { if (stm2!=null) + ifElseStatement.setUsingSemicolonForElseStatement(true); + else if (ifElseStatement!=null) + ifElseStatement.setUsingSemicolonForIfStatement(true); + widen(ifElseStatement, null, getToken(0)); + } + ] + + ) + | + ( + (stm = SingleStatement() + | stm = Block()) + + ( )* + ) + | + LOOKAHEAD( { stm==null && getAsBoolean(Feature.allowUnsupportedStatements) && isOpaqueStatementAhead() } ) + stm = UnsupportedStatement() ( )* + } catch (ParseException ex) { + if ( errorRecovery ) { + ifElseStatement = null; + parseErrors.add(ex); + error_skipto(ST_SEMICOLON); + stm = null; + } else { + throw ex; + } + } + + ) + + { + token_source.positionTrailingComments(); + return ifElseStatement!=null ? ifElseStatement : stm; + } +} + +Statement SingleStatement() : +{ + Statement stm = null; + List> with = null; + Token first = getToken(1); +} +{ + ( + LOOKAHEAD({ Dialect.ORACLE.name().equals(getAsString(Feature.dialect)) + && (getToken(1).kind == K_DECLARE || getToken(1).kind == K_BEGIN) }) + stm = OracleBlock() + | + LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + && ((getToken(1).kind == K_ALTER && (getToken(2).kind == K_FUNCTION || getToken(2).kind == K_PROCEDURE)) + || (getToken(1).kind == K_CREATE && (getToken(2).kind == K_FUNCTION || getToken(2).kind == K_PROCEDURE + || (getToken(2).kind == K_OR && getToken(3).kind == K_ALTER + && (getToken(4).kind == K_FUNCTION || getToken(4).kind == K_PROCEDURE))))) }) + stm = SqlServerRoutine() + | + LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_INSERT && "BULK".equalsIgnoreCase(getToken(2).image) }) + stm = InsertBulk() + | + LOOKAHEAD(3) ( + [ LOOKAHEAD(2) with=WithList() ] + ( + stm = SelectWithWithItems( with ) { if (with != null) { widen(stm, first, null); } } + | + stm = InsertWithWithItems( with ) + | + stm = UpdateWithWithItems( with ) + | + stm = DeleteWithWithItems( with ) + | + stm = Merge( with ) + ) + ) + | + stm = Select() + | + LOOKAHEAD(3) stm = Upsert() + | + LOOKAHEAD(2) stm = Alter() + | + LOOKAHEAD({ getToken(1).kind == K_RENAME && getToken(2).kind == K_USER + && getToken(3).kind != K_TO && !".".equals(getToken(3).image) }) + stm = RenameUser() + | + // @todo: merge this into the ALTER TABLE statement + stm = RenameTableStatement() + | + stm = Create() + | + LOOKAHEAD( ) stm = DropPolicy() + | + LOOKAHEAD({ getToken(1).kind == K_DROP && getToken(2).kind == K_USER + && "MAPPING".equalsIgnoreCase(getToken(3).image) + && (getToken(4).kind == K_FOR || getToken(4).kind == K_IF) }) + stm = DropUserMapping() + | + stm = Drop() + | + LOOKAHEAD({ getToken(1).kind == K_REASSIGN && "OWNED".equalsIgnoreCase(getToken(2).image) }) + stm = ReassignOwned() + | + stm = Analyze() + | + LOOKAHEAD({ isTableMaintenanceAhead() }) stm = TableMaintenanceStatement() + | + LOOKAHEAD({ isReindexAhead() }) stm = Reindex() + | + LOOKAHEAD({ getToken(1).kind == K_SECURITY && "LABEL".equalsIgnoreCase(getToken(2).image) }) + stm = SecurityLabel() + | + stm = Truncate() + | + stm = Execute() + | + LOOKAHEAD({ getToken(1).kind == K_DO && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) }) + stm = DoStatement() + | + LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_SET && getToken(2).kind == S_IDENTIFIER + && "IDENTITY_INSERT".equalsIgnoreCase(getToken(2).image) + && !"=".equals(getToken(3).image) && !".".equals(getToken(3).image) }) + stm = SetIdentityInsert() + | + LOOKAHEAD({ getAsBoolean(Feature.allowUnsupportedStatements) + && getToken(1).kind == K_SET && getToken(2).kind == S_IDENTIFIER + && !"=".equals(getToken(3).image) && !".".equals(getToken(3).image) + && !":=".equals(getToken(3).image) + && ("IDENTITY_INSERT".equalsIgnoreCase(getToken(2).image) + || "ISOLATION".equalsIgnoreCase(getToken(2).image) + && (isInformixIsolationLevel(getToken(3)) + || getToken(3).kind == K_TO && isInformixIsolationLevel(getToken(4)))) }) + stm = OpaqueSetStatement() + | + LOOKAHEAD({ isMySqlSetRoleAhead() }) stm = SetRoleStatement() + | + LOOKAHEAD({ getToken(1).kind == K_SET && getToken(2).kind == K_DEFAULT + && "ROLE".equalsIgnoreCase(getToken(3).image) }) + stm = SetDefaultRoleStatement() + | + LOOKAHEAD({ isMySqlSetPasswordAhead() }) stm = SetPasswordStatement() + | + stm = Set() + | + stm = Reset() + | + stm = Show() + | + stm = RefreshMaterializedView() + | + stm = Use() + | + LOOKAHEAD({ isBeginTransactionAhead() + || getToken(1).kind == K_START && "TRANSACTION".equalsIgnoreCase(getToken(2).image) }) + stm = StartTransaction() + | + LOOKAHEAD({ isKeywordAhead("RELEASE") }) stm = ReleaseSavepointStatement() + | + stm = SavepointStatement() + | + stm = RollbackStatement() + | + stm = Commit() + | + stm = Comment() + | + LOOKAHEAD({ isKeywordAhead("NOTIFY") }) stm = NotifyStatement() + | + stm = Describe() + | + stm = Explain() + | + stm = Declare() + | + stm = Grant() + | + LOOKAHEAD({ isKeywordAhead("REVOKE") }) stm = Revoke(false) + | + LOOKAHEAD({ getToken(1).kind == K_PRAGMA }) stm = PragmaStatement() + | + LOOKAHEAD({ getToken(1).kind == K_INSTALL + || (getToken(1).kind == K_LOAD && getToken(2).kind != K_DATA) + || (getToken(1).kind == K_FORCE && getToken(2).kind == K_INSTALL) }) + stm = ExtensionStatement() + | + LOOKAHEAD({ getToken(1).kind == K_ATTACH }) stm = AttachStatement() + | + LOOKAHEAD({ getToken(1).kind == K_DETACH }) stm = DetachStatement() + | + LOOKAHEAD({ getToken(1).kind == K_CONNECT && getToken(2).kind != K_BY }) + stm = ConnectStatement() + | + LOOKAHEAD({ getToken(1).kind == K_DISCONNECT && getToken(2).kind != K_SESSION }) + stm = DisconnectStatement() + | + LOOKAHEAD({ getToken(1).kind == K_PREPARE }) stm = PrepareStatement() + | + LOOKAHEAD({ getToken(1).kind == K_DEALLOCATE }) stm = DeallocateStatement() + | + LOOKAHEAD({ getToken(1).kind == K_COPY }) stm = CopyStatement() + | + LOOKAHEAD({ getToken(1).kind == K_ASSERT }) stm = AssertStatement() + | + LOOKAHEAD({ getToken(1).kind == K_EXPORT && getToken(2).kind == K_DATA }) stm = ExportDataStatement() + | + LOOKAHEAD({ getToken(1).kind == K_LOAD && getToken(2).kind == K_DATA }) stm = LoadDataStatement() + | + stm = PurgeStatement() + | + stm = SessionStatement() + | + stm = LockStatement() + | + LOOKAHEAD({ Dialect.EXASOL.name().equals(getAsString(Feature.dialect)) }) ( stm = Import() | stm = Export() ) + ) + { + linkStatement(stm, first); + return stm; + } +} + +OracleBlock OracleBlock(): +{ + OracleBlock block = new OracleBlock(); + OracleBlock.VariableDeclaration declaration; + OracleBlock.ExceptionHandler handler; + Statements statements; +} +{ + [ declaration=OracleVariableDeclaration() { block.getDeclarations().add(declaration); } + ( LOOKAHEAD({ getToken(1).kind != K_BEGIN }) + declaration=OracleVariableDeclaration() { block.getDeclarations().add(declaration); } )* ] + statements=OracleBlockStatements() { block.setStatements(statements); } + [ LOOKAHEAD({ isKeywordAhead("EXCEPTION") }) AccessKeyword("EXCEPTION") + handler=OracleExceptionHandler() { block.getExceptionHandlers().add(handler); } + ( handler=OracleExceptionHandler() { block.getExceptionHandlers().add(handler); } )* ] + + { block.setSemicolonAfterEnd(true); return block; } +} + +OracleBlock.VariableDeclaration OracleVariableDeclaration(): +{ + OracleBlock.VariableDeclaration declaration = new OracleBlock.VariableDeclaration(); + String name; ColDataType dataType; Expression initializer; +} +{ + name=RelObjectName() { declaration.setName(name); } + [ LOOKAHEAD({ isKeywordAhead("CONSTANT") }) AccessKeyword("CONSTANT") { declaration.setConstant(true); } ] + dataType=ColDataType() { declaration.setDataType(dataType); } + [ { declaration.setNotNull(true); } ] + [ ( ":=" | { declaration.setInitializerOperator("DEFAULT"); } ) + initializer=Expression() { declaration.setInitializer(initializer); } ] + + { requireDdlSyntax((!declaration.isConstant() && !declaration.isNotNull()) || declaration.getInitializer() != null, + "A CONSTANT or NOT NULL variable requires an initializer"); return declaration; } +} + +Statements OracleBlockStatements(): +{ Statements statements = new Statements(); Statement statement; } +{ + statement=OracleBlockStatement() { statements.add(statement); } + ( LOOKAHEAD({ getToken(1).kind != K_END && getToken(1).kind != K_WHEN && !isKeywordAhead("EXCEPTION") }) + statement=OracleBlockStatement() { statements.add(statement); } )* + { return statements; } +} + +Statement OracleBlockStatement(): +{ Statement statement; Expression target; Expression value; Token first = getToken(1); } +{ + ( + LOOKAHEAD({ getToken(1).kind == K_DECLARE || getToken(1).kind == K_BEGIN }) statement=OracleBlock() + | LOOKAHEAD(Column() ":=") target=Column() ":=" value=Expression() + { statement = new OracleAssignment(target, value); } + | LOOKAHEAD(JdbcNamedParameter() ":=") target=JdbcNamedParameter() ":=" value=Expression() + { statement = new OracleAssignment(target, value); } + | { statement = new OracleNullStatement(); } + | LOOKAHEAD(ColumnIdentifier() ("(" | )) + statement=OracleImplicitCall() + | statement=SingleStatement() + ) + { linkStatement(statement, first); return statement; } +} + +Execute OracleImplicitCall(): +{ Execute call = new Execute(); ObjectNames name; ExpressionList arguments; } +{ + name=ColumnIdentifier() { call.setName(name.getNames()); call.setExecType(Execute.ExecType.IMPLICIT); + requireDdlSyntax(!Arrays.asList("RAISE", "RETURN", "GOTO", "EXIT", "CONTINUE", "LOOP", "WHILE", "FOR") + .contains(call.getName().toUpperCase(java.util.Locale.ROOT)), "Unsupported PL/SQL control statement"); } + [ LOOKAHEAD(1, "(") arguments=ExpressionList() { call.setExprList(arguments); } ] + { return call; } +} + +OracleBlock.ExceptionHandler OracleExceptionHandler(): +{ OracleBlock.ExceptionHandler handler = new OracleBlock.ExceptionHandler(); String name; Statements body; } +{ + name=TypeDdlName() { handler.getExceptions().add(name); } + ( name=TypeDdlName() { handler.getExceptions().add(name); } )* + body=OracleBlockStatements() { handler.setStatements(body); return handler; } +} + +DoStatement DoStatement(): +{ + DoStatement statement = new DoStatement(); + String language; + Token code; +} +{ + + ( + LOOKAHEAD({ isKeywordAhead("LANGUAGE") }) + AccessKeyword("LANGUAGE") language=RelObjectName() code= + { statement.setLanguage(language); statement.setLanguageBeforeCode(true); } + | + code= + [ LOOKAHEAD({ isKeywordAhead("LANGUAGE") }) AccessKeyword("LANGUAGE") + language=RelObjectName() { statement.setLanguage(language); } ] + ) + { statement.setCode(stringValue(code)); return statement; } +} + +Block Block() #Block : { + Block block; +} +{ + block = BeginEndBlock() + [LOOKAHEAD(2) { block.setSemicolonAfterEnd(true); widen(block, null, getToken(0)); } ] + { + return block; + } +} + +/** BEGIN, statements and END; unlike Block(), it leaves a following semicolon to the caller. */ +Block BeginEndBlock(): { + Statements stmts = new Statements(); + List list = new ArrayList(); + Statement stm; + Block block = new Block(); + Token first = getToken(1); +} +{ + + ()* + ( + stm = SingleStatement() + | stm = Block() + ) + + { list.add(stm); } + + ( + ( + ( + stm = SingleStatement() + | stm = Block() + ) + + { list.add(stm); } + ) + )* + + { + stmts.setStatements(list); + block.setStatements(stmts); + } + + + { + linkAST(block, first); + return block; + } +} + +Statements Statements() #Statements: { + Statements stmts = new Statements(); + IfElseStatement ifElseStatement = null; + Statement stm = null; + Statement stm2 = null; + Token ifFirst = null; + Expression condition; +} +{ + { + // Opaque roots can be first; capture them before known grammar lookahead. + while (getToken(1).kind == ST_SEMICOLON) { + getNextToken(); + } + captureOpaqueStatements(stmts); + if (getToken(1).kind == EOF) { + getNextToken(); + return stmts; + } + } + ( + ( + ( )* + { + captureOpaqueStatements(stmts); + if (getToken(1).kind == EOF) { + getNextToken(); + return stmts; + } + } + + try { + ( + { ifFirst = getToken(0); } condition=Condition() + ( stm = SingleStatement() | stm = Block() ) { ifElseStatement = new IfElseStatement(condition, stm); linkAST(ifElseStatement, ifFirst); } + [ LOOKAHEAD(2) + [ { ifElseStatement.setUsingSemicolonForIfStatement(true); } ] + ( stm2 = SingleStatement() | stm2 = Block() ) { ifElseStatement.setElseStatement(stm2); widen(ifElseStatement, null, getToken(0)); } + ] + + { requireStatementBoundary(); stmts.add( ifElseStatement ); } + + [ + LOOKAHEAD(2) + { if (stm2!=null) + ifElseStatement.setUsingSemicolonForElseStatement(true); + else if (ifElseStatement!=null) + ifElseStatement.setUsingSemicolonForIfStatement(true); + widen(ifElseStatement, null, getToken(0)); + } + ] + ) + | + ( + (stm = SingleStatement() + | stm = Block()) + + ( | ) + ) + { + stmts.add(stm); stm=null; + } + + } catch (ParseException ex) { + if ( errorRecovery ) { + parseErrors.add(ex); + error_skipto(ST_SEMICOLON); + stmts.add( null ); + } else { + throw ex; + } + } + + ) + + ( LOOKAHEAD(2) + ( )* + { + captureOpaqueStatements(stmts); + if (getToken(1).kind == EOF) { + getNextToken(); + return stmts; + } + } + try { + ( + { ifFirst = getToken(0); } condition=Condition() + ( stm = SingleStatement() | stm = Block() ) { ifElseStatement = new IfElseStatement(condition, stm); linkAST(ifElseStatement, ifFirst); } + [ LOOKAHEAD(2) + [ { ifElseStatement.setUsingSemicolonForIfStatement(true); } ] + ( stm2 = SingleStatement() | stm2 = Block() ) { ifElseStatement.setElseStatement(stm2); widen(ifElseStatement, null, getToken(0)); } + ] + + { requireStatementBoundary(); stmts.add( ifElseStatement ); } + ) + | + ( + stm = SingleStatement() + | stm = Block() + ) { requireStatementBoundary(); stmts.add(stm); stm=null; } + } catch (ParseException ex) { + if ( errorRecovery ) { + parseErrors.add(ex); + error_skipto(ST_SEMICOLON); + stmts.add( null ); + } else { + throw ex; + } + } + + ( LOOKAHEAD(2) { if (stm2!=null) + ifElseStatement.setUsingSemicolonForElseStatement(true); + else if (ifElseStatement!=null) + ifElseStatement.setUsingSemicolonForIfStatement(true); + widen(ifElseStatement, null, getToken(0)); + } )* + + )* + + [ + LOOKAHEAD( { getAsBoolean(Feature.allowUnsupportedStatements) && isOpaqueStatementAhead() } ) + stm = UnsupportedStatement() + { + if (!( (UnsupportedStatement) stm).isEmpty()) { + stmts.add( stm ); + } + } + ] + + )* + + + { + token_source.positionTrailingComments(); + return stmts; + } +} + +JAVACODE +UnsupportedStatement captureStatementTokens(Token statementStart) { + List tokenImages = new ArrayList(); + Token statementEnd = getToken(0); + for (Token t = statementStart.next; ; t = t.next) { + tokenImages.add(t.image); + if (t == statementEnd) { + break; + } + } + UnsupportedStatement statement = new UnsupportedStatement(tokenImages); + linkAST(statement, statementStart.next, statementEnd); + return statement; +} + +JAVACODE +List error_skipto(int kind) { + ArrayList tokenImages = new ArrayList(); + ParseException e = generateParseException("test"); + Token t; + do { + t = getNextToken(); + if (t.kind != kind && t.kind != EOF) { + tokenImages.add(t.image); + } + } while (t.kind != kind && t.kind != EOF); + return tokenImages; +} + +LockStatement LockStatement(): { + LockStatement statement = new LockStatement(); + LockStatement.Target target; + LockMode mode; + Token waitSeconds; +} { + + { statement.setUseTableKeyword(false); } + [ LOOKAHEAD(1, ) { statement.setUseTableKeyword(true); } ] + target=LockTarget() { statement.getTargets().add(target); } + ( "," target=LockTarget() { statement.getTargets().add(target); } )* + [ mode=TableLockMode() { statement.setLockMode(mode); } ] + [ { statement.setNoWait(true); } + | waitSeconds= { statement.setWaitSeconds(Long.valueOf(waitSeconds.image)); } ] + { return statement; } +} + +LockStatement.Target LockTarget(): +{ TableReference reference; } +{ + reference=RelationTarget() + { + return RelationOptions.lockTarget(reference); + } +} + +/** Shared PostgreSQL relation_expr target, including ONLY (qualified_name). */ +TableReference RelationTarget(): +{ + Table table; + boolean only = false; + boolean descendants = false; + boolean parenthesized = false; + TableReference target; +} +{ + [ LOOKAHEAD(2) { only = true; } ] + ( "(" table=Table() ")" { parenthesized = true; } | table=Table() ) + [ "*" { descendants = true; } ] + { + requireDdlSyntax(!only || !descendants, "ONLY and * cannot describe the same relation target"); + requireDdlSyntax(!parenthesized || only, "Parenthesized relation targets require ONLY"); + target = new TableReference(table); + target.setOnly(only); + target.setIncludeDescendants(descendants); + target.setParenthesized(parenthesized); + return target; + } +} + +LockMode TableLockMode(): { + LockMode mode; +} { + ( + ( { mode = LockMode.RowShare; } + | { mode = LockMode.RowExclusive; } ) + | { mode = LockMode.Share; } + [ { mode = LockMode.ShareRowExclusive; } + | { mode = LockMode.ShareUpdate; } + [ { mode = LockMode.ShareUpdateExclusive; } ] ] + | { mode = LockMode.Exclusive; } + | AccessKeyword("ACCESS") ( { mode = LockMode.AccessShare; } + | { mode = LockMode.AccessExclusive; } ) + ) + { return mode; } +} + +LikeClause LikeClause(): { + LikeClause likeClause = new LikeClause(); + Table table; + List> columnsList; + boolean including; + String option; +} { + + table = Table() { likeClause.setTable(table); } + [ LOOKAHEAD(2) "(" columnsList = ColumnSelectItemsList() ")" { likeClause.setColumnsList(columnsList); }] + ( + ( + { including = true; } + | { including = false; } + ) + option=RelObjectName() + { + likeClause.addOption(likeOptionKind(option), including); + } + )* + + { + return likeClause; + } +} + +Export Export() #Export: { + Export export = new Export(); + Table table; + ParenthesedExpressionList columns; + ParenthesedSelect select; + ExportIntoItem exportIntoItem; +} { + + ( + table = Table() { export.setTable(table); } + [ columns = ParenthesedColumnList() { export.setColumns(columns); } ] + | select = ParenthesedSelect() { export.setSelect(select); } + ) + + + exportIntoItem = ExportIntoItem() { export.setExportIntoItem(exportIntoItem); } + + { + return export; + } +} + +Import Import() #Import: { + Import impt = new Import(); + Table table; + ParenthesedExpressionList columns; + List importColumns; + ImportFromItem fromItem; +} { + + [ + + ( + table = Table() { impt.setTable(table); } + [ columns = ParenthesedColumnList() { impt.setColumns(columns); } ] + | importColumns = ImportColumns() { impt.setImportColumns(importColumns); } + ) + ] + + + fromItem = ImportFromItem() { impt.setFromItem(fromItem); } + + { + return impt; + } +} + +Import SubImport() #SubImport: { + Import impt = new Import(); + List importColumns; + ImportFromItem fromItem; +} { + "(" + + [ importColumns = ImportColumns() { impt.setImportColumns(importColumns); } ] + + + fromItem = ImportFromItem() { impt.setFromItem(fromItem); } + ")" + + { + return impt; + } +} + +List ImportColumns(): { + ImportColumn importColumn; + List importColumns = new ArrayList(); +} { + "(" + ( + importColumn = ColumnDefinition() + | importColumn = LikeClause() + ) + { importColumns.add(importColumn); } + + ( + "," + ( + importColumn = ColumnDefinition() + | importColumn = LikeClause() + ) + { importColumns.add(importColumn); } + )* + ")" + + { + return importColumns; + } +} + +ExportIntoItem ExportIntoItem(): { + ExportIntoItem exportIntoItem; + ErrorClause errorClause; +} { + ( + exportIntoItem = DBMSDestination() + | exportIntoItem = FileDestination() + | exportIntoItem = ScriptSourceDestination() + ) + [ LOOKAHEAD(2) errorClause = ErrorClause() { exportIntoItem.setErrorClause(errorClause); } ] + + { + return exportIntoItem; + } +} + +ImportFromItem ImportFromItem(): { + ImportFromItem importFromItem; + ErrorClause errorClause; +} { + ( + importFromItem = DBMSSource() + | importFromItem = FileSource() + | importFromItem = ScriptSourceDestination() + ) + [ LOOKAHEAD(2) errorClause = ErrorClause() { importFromItem.setErrorClause(errorClause); } ] + + { + return importFromItem; + } +} + +DBMSDestination DBMSDestination() #DBMSDestination: { + DBMSDestination dbmsDestination = new DBMSDestination(); + DBMSType dbmsType; + ConnectionDefinition connectionDefinition; + Table table; + ExpressionList columns; + StringValue statement; + List dbmsTableDestinationOptions; +} { + dbmsType = DBMSType() { dbmsDestination.setDestinationType(dbmsType); } + + connectionDefinition = ConnectionDefinition() { dbmsDestination.setConnectionDefinition(connectionDefinition); } + + ( + table = Table() { dbmsDestination.setTable(table); } + [ LOOKAHEAD(2) columns = ParenthesedColumnList() { dbmsDestination.setColumns(columns); } ] + [ LOOKAHEAD(2) dbmsTableDestinationOptions = DBMSTableDestinationOptionList() { dbmsDestination.setDBMSTableDestinationOptions(dbmsTableDestinationOptions); } ] + | statement = ImportExportStatement() { dbmsDestination.setStatement(statement); } + ) + + { + return dbmsDestination; + } +} + +DBMSTableDestinationOption DBMSTableDestinationOption(): { + DBMSTableDestinationOption dbmsTableDestinationOption; + + Token token; + Token token2; + Token token3; +} { + ( + ( + token = + | token = + ) { dbmsTableDestinationOption = new DBMSTableDestinationOption(token.image); } + | token = token2 = token3 = { dbmsTableDestinationOption = new DBMSTableDestinationOption(token.image + " " + token2.image, stringValue(token3)); } + ) + + { + return dbmsTableDestinationOption; + } +} + +List DBMSTableDestinationOptionList(): { + List dbmsTableDestinationOptions = new ArrayList(); + DBMSTableDestinationOption dbmsTableDestinationOption; +} { + ( LOOKAHEAD(2) dbmsTableDestinationOption = DBMSTableDestinationOption() { dbmsTableDestinationOptions.add(dbmsTableDestinationOption); } )+ + { + return dbmsTableDestinationOptions; + } +} + +DBMSSource DBMSSource() #DBMSSource: { + DBMSSource dbmsSource = new DBMSSource(); + DBMSType dbmsType; + ConnectionDefinition connectionDefinition; + Table table; + ExpressionList columns; + List statements; +} { + dbmsType = DBMSType() { dbmsSource.setSourceType(dbmsType); } + + connectionDefinition = ConnectionDefinition() { dbmsSource.setConnectionDefinition(connectionDefinition); } + + ( + table = Table() { dbmsSource.setTable(table); } + [ LOOKAHEAD(2) columns = ParenthesedColumnList() { dbmsSource.setColumns(columns); } ] + | statements = ImportExportStatementsList() { dbmsSource.setStatements(statements); } + ) + { + return dbmsSource; + } +} + +DBMSType DBMSType(): { + DBMSType dbmsType; + Token tk1; + Token tk2 = null; +} { + ( + tk1= + | tk1= + | tk1= + [ "=" tk2=] + ) { dbmsType = new DBMSType(tk1.image, tk2 == null ? null : tk2.image); } + { + return dbmsType; + } +} + +FileType FileType(): { + FileType fileType; + Token tk; +} { + ( + tk= + | tk= + ) { fileType = new FileType(tk.image); } + { + return fileType; + } +} + +StringValue ImportExportStatement() #ImportExportStatement: { + StringValue statement; +} { + token = + { + statement = stringValue(token); + linkAST(statement, jjtThis); + return statement; + } +} + +List ImportExportStatementsList(): { + List statements = new ArrayList(); + StringValue statement; +} { + ( statement = ImportExportStatement() { statements.add(statement); } )+ + + { + return statements; + } +} + +StringValue File() #File: { + Token token; +} { + token = + { + StringValue file = stringValue(token); + linkAST(file, jjtThis); + return file; + } +} + +List FileList(): { + List files = new ArrayList(); + StringValue file; +} { + ( file = File() { files.add(file); } )+ + { + return files; + } +} + +ConnectionFileDefinition ConnectionFileDefinition(): { + ConnectionDefinition connectionDefinition = null; + List files; +} { + connectionDefinition = ConnectionOrCloudConnectionDefinition() + files = FileList() + { + return new ConnectionFileDefinition(connectionDefinition, files); + } +} + +List ConnectionFileDefinitionList(): { + List connectionFileDefinitions = new ArrayList(); + ConnectionFileDefinition connectionFileDefinition; +} { + ( LOOKAHEAD(2) connectionFileDefinition = ConnectionFileDefinition() { connectionFileDefinitions.add(connectionFileDefinition); } )+ + { + return connectionFileDefinitions; + } +} + +CSVColumn CSVDestinationColumn(): { + CSVColumn csvColumn; + + Token token; + Token token2; +} { + ( + LOOKAHEAD(2) + token= ".." token2= { csvColumn = new CSVColumn(Long.valueOf(token.image), Long.valueOf(token2.image)); } + | token= { csvColumn = new CSVColumn(Long.valueOf(token.image)); } + [ "=" token = { csvColumn.setFormat(stringValue(token)); }] + [ + + "=" + ( + token= + | token= + | token= + ) + { csvColumn.setDelimit(token.image); } + ] + ) + { + return csvColumn; + } +} + +List CSVDestinationColumnList(): { + List csvColumns = new ArrayList(); + CSVColumn csvColumn; +} { + csvColumn = CSVDestinationColumn() { csvColumns.add(csvColumn); } + ( "," csvColumn = CSVDestinationColumn() { csvColumns.add(csvColumn); } )* + { + return csvColumns; + } +} + +CSVColumn CSVSourceColumn(): { + CSVColumn csvColumn; + + Token token; + Token token2; +} { + ( + LOOKAHEAD(2) + token= ".." token2= { csvColumn = new CSVColumn(Long.valueOf(token.image), Long.valueOf(token2.image)); } + | token= { csvColumn = new CSVColumn(Long.valueOf(token.image)); } + [ "=" token = { csvColumn.setFormat(stringValue(token)); }] + ) + { + return csvColumn; + } +} + +List CSVSourceColumnList(): { + List csvColumns = new ArrayList(); + CSVColumn csvColumn; +} { + csvColumn = CSVSourceColumn() { csvColumns.add(csvColumn); } + ( "," csvColumn = CSVSourceColumn() { csvColumns.add(csvColumn); } )* + { + return csvColumns; + } +} + +FBVColumn FBVDestinationColumn(): { + FBVColumn fbvColumn; + + Token token; + Token token2; +} { + ( + token= "=" token2= { fbvColumn = new FBVColumn(token.image, new LongValue(token2.image)); } + | ( token= | token= ) "=" token2= { fbvColumn = new FBVColumn(token.image, stringValue(token2)); } + | token= "=" ( token2= | token2= ) { fbvColumn = new FBVColumn(token.image, token2.image); } + ) + { + return fbvColumn; + } +} + +List FBVDestinationColumnList(): { + List fbvColumns = new ArrayList(); + FBVColumn fbvColumn; + boolean precedesComma; +} { + fbvColumn = FBVDestinationColumn() { fbvColumns.add(fbvColumn); } + ( + { precedesComma = false; } + ["," { precedesComma = true; }] + fbvColumn = FBVDestinationColumn() { fbvColumn.setPrecedesComma(precedesComma); fbvColumns.add(fbvColumn); } + )* + { + return fbvColumns; + } +} + +FBVColumn FBVSourceColumn(): { + FBVColumn fbvColumn; + + Token token; + Token token2; +} { + ( + ( token= | token= ) "=" token2= { fbvColumn = new FBVColumn(token.image, new LongValue(token2.image)); } + | ( token= | token= ) "=" token2= { fbvColumn = new FBVColumn(token.image, stringValue(token2)); } + | token= "=" ( token2= | token2= ) { fbvColumn = new FBVColumn(token.image, token2.image); } + ) + { + return fbvColumn; + } +} + +List FBVSourceColumnList(): { + List fbvColumns = new ArrayList(); + FBVColumn fbvColumn; + boolean precedesComma; +} { + fbvColumn = FBVSourceColumn() { fbvColumns.add(fbvColumn); } + ( + { precedesComma = false; } + ["," { precedesComma = true; }] + fbvColumn = FBVSourceColumn() { fbvColumn.setPrecedesComma(precedesComma); fbvColumns.add(fbvColumn); } + )* + { + return fbvColumns; + } +} + +FileOption FileDestinationOption(): { + FileOption fileOption; + + Token token; + Token token2; + Token token3; +} { + ( + ( token= | token= ) { fileOption = new FileOption(token.image); } + | token= token2= token3= { fileOption = new FileOption(token.image + " " + token2.image + " " + token3.image); } + | ( token= | token= | token= ) "=" token2= { fileOption = new FileOption(token.image, stringValue(token2)); } + | ( + token= token2= "=" token3= + | token= ( token2= | token2= ) "=" token3= + ) + { fileOption = new FileOption(token.image + " " + token2.image, stringValue(token3)); } + | token= "=" ( token2= | token2= | token2= ) { fileOption = new FileOption(token.image, token2.image); } + ) + { + return fileOption; + } +} + +List FileDestinationOptionList(): { + List fileOptions = new ArrayList(); + FileOption fileOption; +} { + ( LOOKAHEAD(2) fileOption = FileDestinationOption() { fileOptions.add(fileOption); } )+ + { + return fileOptions; + } +} + +FileOption FileSourceOption(): { + FileOption fileOption; + + Token token; + Token token2; + Token token3; +} { + ( + ( token= | token= | token= ) { fileOption = new FileOption(token.image); } + | ( + ( token= | token= ) "=" token2= { fileOption = new FileOption(token.image, stringValue(token2)); } + | token= "=" token2= { fileOption = new FileOption(token.image, new LongValue(token2.image)); } + ) + | LOOKAHEAD(2) + ( + token= token2= "=" token3= + | token= ( token2= | token2= ) "=" token3= + ) + { fileOption = new FileOption(token.image + " " + token2.image, stringValue(token3)); } + | token= token2= "=" token3= + { fileOption = new FileOption(token.image + " " + token2.image, new LongValue(token3.image)); } + ) + { + return fileOption; + } +} + +List FileSourceOptionList(): { + List fileOptions = new ArrayList(); + FileOption fileOption; +} { + ( LOOKAHEAD(2) fileOption = FileSourceOption() { fileOptions.add(fileOption); } )+ + { + return fileOptions; + } +} + +FileDestination FileDestination() #FileDestination: { + FileDestination fileDestination = new FileDestination(); + FileType fileType; + List connectionFileDefinitions; + List files; + List csvColumns; + List fbvColumns; + List fileOptions; + CertificateVerification certificateVerification; +} { + ( + fileType = FileType() { fileDestination.setDestinationType(fileType); } + connectionFileDefinitions = ConnectionFileDefinitionList() { fileDestination.setConnectionFileDefinitions(connectionFileDefinitions); } + | { fileDestination.setLocal(true); } + [ { fileDestination.setSecure(true); }] + + fileType = FileType() { fileDestination.setDestinationType(fileType); } + files = FileList() + { + connectionFileDefinitions = new ArrayList(); + connectionFileDefinitions.add(new ConnectionFileDefinition(files)); + } + ) + { fileDestination.setConnectionFileDefinitions(connectionFileDefinitions); } + + [ + LOOKAHEAD(2) + "(" + ( + csvColumns = CSVDestinationColumnList() { fileDestination.setCSVColumns(csvColumns); } + | fbvColumns = FBVDestinationColumnList() { fileDestination.setFBVColumns(fbvColumns); } + ) + ")" + ] + + [ LOOKAHEAD(2) fileOptions = FileDestinationOptionList() { fileDestination.setFileOptions(fileOptions); } ] + + [ LOOKAHEAD(2) certificateVerification = CertificateVerification() { fileDestination.setCertificateVerification(certificateVerification); } ] + + { + return fileDestination; + } +} + +FileSource FileSource() #FileSource: { + FileSource fileSource = new FileSource(); + FileType fileType; + List connectionFileDefinitions; + List files; + List csvColumns; + List fbvColumns; + List fileOptions; + CertificateVerification certificateVerification; +} { + ( + fileType = FileType() { fileSource.setSourceType(fileType); } + connectionFileDefinitions = ConnectionFileDefinitionList() { fileSource.setConnectionFileDefinitions(connectionFileDefinitions); } + | { fileSource.setLocal(true); } + [ { fileSource.setSecure(true); }] + + fileType = FileType() { fileSource.setSourceType(fileType); } + files = FileList() + { + connectionFileDefinitions = new ArrayList(); + connectionFileDefinitions.add(new ConnectionFileDefinition(files)); + } + ) + { fileSource.setConnectionFileDefinitions(connectionFileDefinitions); } + + [ + LOOKAHEAD(2) + "(" + ( + csvColumns = CSVSourceColumnList() { fileSource.setCSVColumns(csvColumns); } + | fbvColumns = FBVSourceColumnList() { fileSource.setFBVColumns(fbvColumns); } + ) + ")" + ] + + [ LOOKAHEAD(2) fileOptions = FileSourceOptionList() { fileSource.setFileOptions(fileOptions); } ] + + [ LOOKAHEAD(2) certificateVerification = CertificateVerification() { fileSource.setCertificateVerification(certificateVerification); } ] + + { + return fileSource; + } +} + +CertificateVerification CertificateVerification(): { + CertificateVerification certificateVerification = new CertificateVerification(); + Token token; +} { + ( + ( + { certificateVerification.setIgnoreCertificate(true); } + | { certificateVerification.setVerifyCertificate(true); } + ) + + [ + LOOKAHEAD(2) + + token = { certificateVerification.setPublicKey(stringValue(token)); } + ] + | + token = { certificateVerification.setPublicKey(stringValue(token)); } + ) + { + return certificateVerification; + } +} + +ScriptSourceDestination ScriptSourceDestination(): { + ScriptSourceDestination scriptSourceDestination = new ScriptSourceDestination(); + ConnectionDefinition connectionDefinition; + Table script; + String property; + StringValue value; + + Token token; +} { + + script = Table() { scriptSourceDestination.setScript(script); } + + [ LOOKAHEAD(2) connectionDefinition = ConnectionDefinition() { scriptSourceDestination.setConnectionDefinition(connectionDefinition); } ] + + [ + LOOKAHEAD(2) + { + List properties = new ArrayList(); + List values = new ArrayList(); + scriptSourceDestination.setProperties(properties); + scriptSourceDestination.setValues(values); + } + + + ( + LOOKAHEAD(2) + property = RelObjectName() "=" token = { value = stringValue(token); } + { + properties.add(property); + values.add(value); + } + )+ + ] + + { + return scriptSourceDestination; + } +} + +UserIdentification UserIdentification(): { + UserIdentification userIdentification = new UserIdentification(); + Token token; +} { + + token= { userIdentification.setUser(stringValue(token)); } + + token= { userIdentification.setPassword(stringValue(token)); } + + { + return userIdentification; + } +} + +ConnectionDefinition ConnectionDefinition(): { + ConnectionDefinition connectionDefinition = new ConnectionDefinition(); + String connectionObjectName; + UserIdentification userIdentification; + CertificateVerification certificateVerification; + + Token token; +} { + + ( + connectionObjectName = RelObjectName() { connectionDefinition.setConnectionObjectName(connectionObjectName); } + | token= { connectionDefinition.setConnectionDefinition(stringValue(token)); } + ) + + [ LOOKAHEAD(2) userIdentification = UserIdentification() { connectionDefinition.setUserIdentification(userIdentification); } ] + + [ LOOKAHEAD(2) certificateVerification = CertificateVerification() { connectionDefinition.setCertificateVerification(certificateVerification); } ] + + { + return connectionDefinition; + } +} + +ConnectionDefinition CloudConnectionDefinition(): { + CloudConnectionDefinition connectionDefinition = new CloudConnectionDefinition(); + String connectionObjectName; + UserIdentification userIdentification; + + Token token; + Token token2; +} { + + ( + token = { connectionDefinition.setStorage(token.image); } + | token = token2 = { connectionDefinition.setStorage(token.image + " " + token2.image); } + ) + + ( + connectionObjectName = RelObjectName() { connectionDefinition.setConnectionObjectName(connectionObjectName); } + | token = { connectionDefinition.setConnectionDefinition(stringValue(token)); } + ) + + [ userIdentification = UserIdentification() { connectionDefinition.setUserIdentification(userIdentification); } ] + + { + return connectionDefinition; + } +} + +ConnectionDefinition ConnectionOrCloudConnectionDefinition(): { + ConnectionDefinition connectionDefinition; +} { + ( + LOOKAHEAD(2) connectionDefinition = CloudConnectionDefinition() + | connectionDefinition = ConnectionDefinition() + ) + { + return connectionDefinition; + } +} + +ErrorClause ErrorClause(): { + ErrorClause errorClause = new ErrorClause(); + ErrorDestination errorDestination; + Expression expression; + RejectClause rejectClause; + + Token token; +} { + ( + + errorDestination = ErrorDestination() { errorClause.setErrorDestination(errorDestination); } + [ + LOOKAHEAD(2) + "(" + expression = Expression() { errorClause.setExpression(expression); } + ")" + ] + [ + LOOKAHEAD(2) + ( + { errorClause.setReplace(true); } + | { errorClause.setTruncate(true); } + ) + ] + [ LOOKAHEAD(2) rejectClause = RejectClause() { errorClause.setRejectClause(rejectClause); } ] + | rejectClause = RejectClause() { errorClause.setRejectClause(rejectClause); } + ) + + { + return errorClause; + } +} + +RejectClause RejectClause(): { + RejectClause rejectClause = new RejectClause(); +} { + + ( + token= { rejectClause.setLimit(new LongValue(token.image)); } + | + ) + + [ LOOKAHEAD(2) { rejectClause.setErrors(true); } ] + + { + return rejectClause; + } +} + +ErrorDestination ErrorDestination(): { + ErrorDestination errorDestination; +} { + ( + LOOKAHEAD(2) errorDestination = CSVFileDestination() + | errorDestination = Table() + ) + + { + return errorDestination; + } +} + +CSVFileDestination CSVFileDestination(): { + CSVFileDestination csvFileDestination = new CSVFileDestination(); + ConnectionDefinition connectionDefinition; + StringValue file; +} { + ( + + connectionDefinition = ConnectionOrCloudConnectionDefinition() { csvFileDestination.setConnectionDefinition(connectionDefinition); } + | { csvFileDestination.setLocal(true); } + [ { csvFileDestination.setSecure(true); } ] + + ) + + file = File() { csvFileDestination.setFile(file); } + + { + return csvFileDestination; + } +} + +DeclareStatement Declare(): { + UserVariable userVariable; + ColDataType colDataType; + Expression defaultExpr = null; + DeclareStatement stmt = new DeclareStatement(); + String typeName; + String columnName; + ColumnDefinition colDef; +} { + userVariable = UserVariable() + ( + LOOKAHEAD(2) ( + "(" colDef = ColumnDefinition() + { + stmt.withUserVariable(userVariable) + .withDeclareType(DeclareType.TABLE) + .addColumnDefinition(colDef); + } + ("," colDef = ColumnDefinition() { stmt.addColumnDefinition(colDef); })* ")" + ) + | + typeName = RelObjectName() + { + stmt.withUserVariable(userVariable) + .withDeclareType(DeclareType.AS) + .withTypeName(typeName); + } + | + (colDataType = ColDataType() ["=" defaultExpr = Expression()] + { + stmt.withDeclareType(DeclareType.TYPE) + .addType(userVariable, colDataType, defaultExpr); + } + ("," userVariable = UserVariable() colDataType = ColDataType() { defaultExpr = null; } + ["=" defaultExpr = Expression()] { stmt.addType(userVariable, colDataType, defaultExpr); } )* + ) + ) + { + return stmt; + } +} + +SessionStatement SessionStatement(): +{ + SessionStatement sessionsStatement; + Token actionToken; + Token idToken = null; + String id = null; +} +{ + ( | ) + ( + actionToken = + | + actionToken = + | + actionToken = + | + actionToken = + | + actionToken = + ) + + [ + ( + idToken = + | + idToken = + | + idToken = + | + idToken = + ) { id = idToken.image; } + + ( + "." + ( + idToken = + | + idToken = + | + idToken = + | + idToken = + ) { id += "." + idToken.image; } + )? + ] + { + sessionsStatement = id!=null + ? new SessionStatement(actionToken.image, id) + : new SessionStatement(actionToken.image); + } + + // options + [ + LOOKAHEAD(2) + ( idToken = | idToken = ) + "=" + ( actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = ) + { sessionsStatement.putOption(idToken.image, actionToken.image ); } + + ( + "," + ( idToken = | idToken = ) + "=" + ( actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = ) + { sessionsStatement.putOption(idToken.image, actionToken.image ); } + )* + ] + + { + //linkAST(sessionsStatement,jjtThis); + return sessionsStatement; + } +} + +/** Preserves existing opaque SET forms while requiring their complete syntax. */ +private UnsupportedStatement OpaqueSetStatement(): { + Token statementStart = getToken(0); + SetIdentityInsertStatement identityInsert; + boolean repeatableRead = false; +} +{ + ( + LOOKAHEAD({ "IDENTITY_INSERT".equalsIgnoreCase(getToken(2).image) }) + identityInsert = SetIdentityInsert() + | + AccessKeyword("ISOLATION") [ ] + ( + LOOKAHEAD({ isKeywordAhead("DIRTY") }) AccessKeyword("DIRTY") + [ LOOKAHEAD({ getToken(1).kind == K_WITH && "WARNING".equalsIgnoreCase(getToken(2).image) }) + AccessKeyword("WARNING") ] + | LOOKAHEAD({ isKeywordAhead("COMMITTED") }) AccessKeyword("COMMITTED") + [ AccessKeyword("COMMITTED") ] + | LOOKAHEAD({ isKeywordAhead("CURSOR") }) AccessKeyword("CURSOR") AccessKeyword("STABILITY") + | { repeatableRead = true; } + ) + [ LOOKAHEAD({ isKeywordAhead("RETAIN") }) AccessKeyword("RETAIN") AccessKeyword("LOCKS") + { requireAccessSyntax(!repeatableRead, "REPEATABLE READ cannot retain update locks"); } ] + ) + { return captureStatementTokens(statementStart); } +} + +SetStatement Set(): { + SetStatement set; +} +{ + + ( + LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == S_IDENTIFIER + && SetStatement.OnOffOption.fromName(getToken(1).image) != null + && !"=".equals(getToken(2).image) && !".".equals(getToken(2).image) }) + set=SqlServerSetOnOffOptions() + | set=SetAssignments() + ) + { return set; } +} + +SetStatement SetAssignments(): { + SetStatement set = new SetStatement(); + Expression exp; + Token tk = null; +} +{ + [LOOKAHEAD(3) (tk = | tk = ) { set.setEffectParameter(tk.image); } ] + SetAssignment(set) + ( + "," + ( LOOKAHEAD(3) SetAssignment(set) + | exp=Expression() { set.getExpressions(set.getCount() - 1).add(exp); widen(set.getExpressions(set.getCount() - 1), null, getToken(0)); } ) + )* + { return set; } +} + +/** The first and subsequent assignments use identical names and operators. */ +void SetAssignment(SetStatement set): +{ + Object name; + Expression expression; + SetStatement.AssignmentOperator operator = SetStatement.AssignmentOperator.NONE; +} +{ + ( + LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET) + || (isKeywordAhead("CHARSET") && !"=".equals(getToken(2).image) + && !":=".equals(getToken(2).image)) }) SetCharacterSet(set) + | + ( LOOKAHEAD(2) { name = "Time Zone"; } + | name=UserVariable() + | name=IdentifierChain() ) + [ "=" { operator = SetStatement.AssignmentOperator.EQUALS; } + | ":=" { operator = SetStatement.AssignmentOperator.COLON_EQUALS; } + | LOOKAHEAD({ getToken(1).kind == K_TO && (getAsString(Feature.dialect) == null + || Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) }) + { operator = SetStatement.AssignmentOperator.TO; } ] + expression=SetAssignmentValue() + { + if (operator == SetStatement.AssignmentOperator.TO + && (name instanceof UserVariable || "Time Zone".equals(name))) { + throw new ParseException("SET TO requires a configuration parameter name"); + } + set.add(name, linkElements(new ExpressionList(expression)), operator); + } + ) + { + for (int i = 0; i < set.getCount(); i++) { + if (set.getAssignmentOperator(i) == SetStatement.AssignmentOperator.TO + && set.getCount() != 1) { + throw new ParseException("SET TO accepts one configuration parameter with a value list"); + } + } + } +} + +/** ON is a setting value even when the preceding separator is TO. */ +Expression SetAssignmentValue(): +{ + Token value; + Expression expression; +} +{ + ( LOOKAHEAD({ getToken(1).kind == K_ON && getToken(0).kind == K_TO }) + value= { expression = linkToLastToken(new Column(value.image)); } + | expression=Expression() ) + { return expression; } +} + +/** Character set selection has a single name or DEFAULT, not an arbitrary expression. */ +void SetCharacterSet(SetStatement set): +{ + String name; + Token value; + Expression expression; +} +{ + ( { name = "CHARACTER SET"; } + | { name = "CHARSET"; } ) + ( value= { expression = stringValue(value); } + | (value= | value= | value=) + { expression = linkToLastToken(new Column(value.image)); } ) + { set.add(name, linkElements(new ExpressionList(expression)), false); } +} + +SetStatement SqlServerSetOnOffOptions(): { + SetStatement set = new SetStatement(); + List options = new ArrayList(); + SetStatement.OnOffOption option; + Token name; + boolean on; +} +{ + name= + { + option = accessEnum(SetStatement.OnOffOption.class, name.image); + options.add(option); + } + ( + "," name= + { + option = accessEnum(SetStatement.OnOffOption.class, name.image); + options.add(option); + } + )* + ( { on = true; } | { on = false; } ) + { + set.setOnOffOptions(new SetStatement.OnOffOptions(options, on)); + return set; + } +} + +ResetStatement Reset(): { + String name; + ResetStatement reset; + Token all; +} +{ + ( LOOKAHEAD(2) {name = "Time Zone"; } | name = RelObjectName() | all = {name = all.image; } ) + { reset = new ResetStatement(name); return reset; } +} + +RenameTableStatement RenameTableStatement(): { + RenameTableStatement renameTableStatement; + Table oldName; + Table newName; + boolean usingTableKeyword=false; + boolean usesIfExistsKeyword=false; + String waitDirective = ""; + Token token; +} +{ + + [ LOOKAHEAD(2) { usingTableKeyword = true; } ] + [ LOOKAHEAD(2) { usesIfExistsKeyword = true; } ] + oldName = Table() + [ ( + token= { waitDirective = "WAIT " + token.image; } + | + { waitDirective = "NOWAIT"; } + ) ] + + newName = Table() + + { + renameTableStatement = new RenameTableStatement(oldName, newName, usingTableKeyword, usesIfExistsKeyword, waitDirective); + } + + ( + "," + oldName = Table() + + newName = Table() + { + renameTableStatement.addTableNames(oldName, newName); + } + )* + + { + return renameTableStatement; + } +} + +/** + * DuckDB's CREATE [OR REPLACE] [TEMPORARY] {MACRO | FUNCTION} [IF NOT EXISTS] name + * (parameters) AS [TABLE] body [, (parameters) AS [TABLE] body ...]. + */ +CreateMacro CreateMacro(boolean isUsingOrReplace) #CreateMacro: +{ + CreateMacro createMacro = new CreateMacro(); + List overloads = new ArrayList(); + ObjectNames name; + CreateMacro.Overload overload; +} +{ + { createMacro.setOrReplace(isUsingOrReplace); } + [ ( | ) { createMacro.setTemporary(true); } ] + ( | { createMacro.setFunctionKeyword(true); } ) + [ LOOKAHEAD(3) { createMacro.setIfNotExists(true); } ] + name=RelObjectNames() { createMacro.setName(String.join(".", name.getNames())); } + overload=MacroOverload() { overloads.add(overload); } + ( LOOKAHEAD(2) "," overload=MacroOverload() { overloads.add(overload); } )* + { createMacro.setOverloads(overloads); } + { return createMacro; } +} + +/** + * One (parameters) AS [TABLE] body of a DuckDB macro, a parameter being name [type] [:= default]. + */ +CreateMacro.Overload MacroOverload(): +{ + CreateMacro.Overload overload = new CreateMacro.Overload(); + List parameters = new ArrayList(); + CreateMacro.Parameter parameter; + Expression expression; + Select select; +} +{ + "(" + [ + parameter=MacroParameter() { parameters.add(parameter); } + ( "," parameter=MacroParameter() { parameters.add(parameter); } )* + ] + ")" { overload.setParameters(parameters); } + + ( + LOOKAHEAD(2) select=Select() { overload.setSelect(select); } + | + expression=Expression() { overload.setExpression(expression); } + ) + { return overload; } +} + +CreateMacro.Parameter MacroParameter(): +{ + String name; + ColDataType type = null; + Expression defaultValue = null; +} +{ + name=RelObjectName() + [ LOOKAHEAD({ getToken(1).kind != K_COMMA && getToken(1).kind != CLOSING_BRACKET && !":=".equals(getToken(1).image) }) type=ColDataType() ] + [ ":=" defaultValue=Expression() ] + { return new CreateMacro.Parameter(name, type, defaultValue); } +} + +/** + * DuckDB's COPY table [(columns)] FROM path [(options)] and + * COPY {table | (query)} TO path [(options)]. + */ +CopyStatement CopyStatement() #CopyStatement: +{ + CopyStatement copyStatement = new CopyStatement(); + ExpressionList columns = null; + List options = new ArrayList(); + String option; + Select select; + Table table; + Token token; +} +{ + + ( + LOOKAHEAD(2) "(" select=Select() ")" { copyStatement.setSelect(select); } + | + table=Table() { copyStatement.setTable(table); } + [ LOOKAHEAD(2) "(" columns=ColumnList() ")" { copyStatement.setColumns(columns); } ] + ) + ( + { copyStatement.setFrom(true); } + | + + ) + ( + token= { copyStatement.setPath(token.image); } + | + token= { copyStatement.setPath(token.image); } + ) + [ LOOKAHEAD({ getToken(1).kind == K_WITH }) + { copyStatement.setWithKeyword(true); } ] + [ + LOOKAHEAD(2) "(" option=DatabaseOption() { options.add(option); } + ( "," option=DatabaseOption() { options.add(option); } )* + ")" { + if (copyStatement.isWithKeyword() + || Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { + requireDdlSyntax(!options.contains(""), "Expected a COPY option"); + } + copyStatement.setOptions(options); + } + ] + { return copyStatement; } +} + +/** + * PREPARE name AS statement, storing a parameterised statement for later EXECUTE. + */ +PrepareStatement PrepareStatement() #PrepareStatement: +{ + PrepareStatement prepareStatement = new PrepareStatement(); + ObjectNames name; + Statement statement; + List parameterTypes = new ArrayList(); + ColDataType parameterType; +} +{ + name=RelObjectNames() + { prepareStatement.setName(String.join(".", name.getNames())); } + [ "(" parameterType=PrepareParameterType() { parameterTypes.add(parameterType); } + ( "," parameterType=PrepareParameterType() { parameterTypes.add(parameterType); } )* + ")" { prepareStatement.setParameterTypes(parameterTypes); } ] + statement=SingleStatement() { prepareStatement.setStatement(statement); } + { return prepareStatement; } +} + +/** PostgreSQL UNKNOWN leaves a parameter type to be inferred from the prepared statement. */ +ColDataType PrepareParameterType(): +{ ColDataType type; Token token; } +{ + ( token= { type = linkToLastToken(new ColDataType(token.image)); } + | type=ColDataType() ) + { return type; } +} + +/** + * DEALLOCATE [PREPARE] name. + */ +DeallocateStatement DeallocateStatement() #DeallocateStatement: +{ + DeallocateStatement deallocateStatement = new DeallocateStatement(); + ObjectNames name; +} +{ + + [ LOOKAHEAD(2) { deallocateStatement.setUsingPrepareKeyword(true); } ] + name=RelObjectNames() { deallocateStatement.setName(String.join(".", name.getNames())); } + { return deallocateStatement; } +} + +/** + * DuckDB's CONNECT target [AS alias], routing the session to a remote database. + */ +ConnectStatement ConnectStatement() #ConnectStatement: +{ + ConnectStatement connectStatement = new ConnectStatement(); + ObjectNames name; + Token token; +} +{ + + ( + token= { connectStatement.setTarget(token.image); } + | + name=RelObjectNames() { connectStatement.setTarget(String.join(".", name.getNames())); } + ) + [ name=RelObjectNames() { connectStatement.setAlias(String.join(".", name.getNames())); } ] + { return connectStatement; } +} + +/** + * DuckDB's DISCONNECT [name]. + */ +DisconnectStatement DisconnectStatement() #DisconnectStatement: +{ + DisconnectStatement disconnectStatement = new DisconnectStatement(); + ObjectNames name; +} +{ + + [ + LOOKAHEAD(2) + name=RelObjectNames() { disconnectStatement.setName(String.join(".", name.getNames())); } + ] + { return disconnectStatement; } +} + +/** + * DuckDB's ATTACH [DATABASE] [IF NOT EXISTS] path [AS alias] [(options)]. + */ +AttachStatement AttachStatement() #AttachStatement: +{ + AttachStatement attachStatement = new AttachStatement(); + List options = new ArrayList(); + String option; + ObjectNames name; + Token token; +} +{ + + // greedy: DATABASE right after ATTACH is the keyword, not the database name + [ LOOKAHEAD(2) { attachStatement.setUsingDatabaseKeyword(true); } ] + [ LOOKAHEAD(2) { attachStatement.setIfNotExists(true); } ] + ( + token= { attachStatement.setDatabasePath(token.image); } + | + name=RelObjectNames() { attachStatement.setDatabasePath(String.join(".", name.getNames())); } + ) + [ name=RelObjectNames() { attachStatement.setAlias(String.join(".", name.getNames())); } ] + [ + LOOKAHEAD(2) "(" option=DatabaseOption() { options.add(option); } + ( "," option=DatabaseOption() { options.add(option); } )* + ")" { attachStatement.setOptions(options); } + ] + { return attachStatement; } +} + +/** + * One ATTACH or COPY option, such as READ_ONLY, TYPE SQLITE or FORMAT PARQUET. Option names and + * values are free-form keywords, so the tokens are collected verbatim up to the next comma or the + * closing bracket. + */ +JAVACODE +String DatabaseOption() { + StringBuilder builder = new StringBuilder(); + int depth = 0; + while (true) { + Token token = getToken(1); + if (token == null || token.kind == EOF) { + break; + } + if (depth == 0 && (",".equals(token.image) || ")".equals(token.image))) { + break; + } + if ("(".equals(token.image)) { + depth++; + } else if (")".equals(token.image)) { + depth--; + } + getNextToken(); + if (builder.length() > 0) { + builder.append(" "); + } + builder.append(token.image); + } + return builder.toString(); +} + +/** + * DuckDB's DETACH [DATABASE] [IF EXISTS] name. + */ +DetachStatement DetachStatement() #DetachStatement: +{ + DetachStatement detachStatement = new DetachStatement(); + ObjectNames name; +} +{ + + // greedy: DATABASE right after DETACH is the keyword, not the database name + [ LOOKAHEAD(2) { detachStatement.setUsingDatabaseKeyword(true); } ] + [ LOOKAHEAD(2) { detachStatement.setIfExists(true); } ] + name=RelObjectNames() { detachStatement.setDatabaseName(String.join(".", name.getNames())); } + { return detachStatement; } +} + +/** + * DuckDB's [FORCE] INSTALL extension [FROM repository] and LOAD extension. + */ +ExtensionStatement ExtensionStatement() #ExtensionStatement: +{ + ExtensionStatement extensionStatement = new ExtensionStatement(); + ObjectNames name; + Token token; +} +{ + [ { extensionStatement.setForce(true); } ] + ( + { extensionStatement.setOperation(ExtensionStatement.Operation.INSTALL); } + | + { extensionStatement.setOperation(ExtensionStatement.Operation.LOAD); } + ) + ( + token= { extensionStatement.setExtensionName(token.image); } + | + name=RelObjectNames() { extensionStatement.setExtensionName(String.join(".", name.getNames())); } + ) + [ + LOOKAHEAD(2) + ( + token= { extensionStatement.setRepository(token.image); } + | + name=RelObjectNames() { extensionStatement.setRepository(String.join(".", name.getNames())); } + ) + ] + { return extensionStatement; } +} + +/** + * DuckDB/SQLite PRAGMA name [(arguments) | = value]. + */ +PragmaStatement PragmaStatement() #PragmaStatement: +{ + PragmaStatement pragmaStatement = new PragmaStatement(); + ObjectNames name; + ExpressionList parameters; + Expression value; +} +{ + name=RelObjectNames() + { pragmaStatement.setName(String.join(".", name.getNames())); } + // greedy: parentheses right after the pragma name are its arguments + [ + LOOKAHEAD(2) + ( + "(" parameters=ExpressionList() ")" { pragmaStatement.setParameters(parameters); } + | + "=" value=SimpleExpression() { pragmaStatement.setValue(value); } + ) + ] + { return pragmaStatement; } +} + +/** + * BigQuery's LOAD DATA {INTO | OVERWRITE} table [(column_definitions)] [OPTIONS (option_list)] + * FROM FILES (file_options) [WITH PARTITION COLUMNS] [WITH CONNECTION connection]. + */ +LoadDataStatement LoadDataStatement() #LoadDataStatement: +{ + LoadDataStatement loadDataStatement = new LoadDataStatement(); + List columnDefinitions = new ArrayList(); + ColumnDefinition columnDefinition; + ExpressionList options; + ObjectNames connectionName; + Table table; +} +{ + + ( | { loadDataStatement.setOverwrite(true); } ) + table=Table() { loadDataStatement.setTable(table); } + [ + LOOKAHEAD(2) "(" + columnDefinition=ColumnDefinition() { columnDefinitions.add(columnDefinition); } + ( "," columnDefinition=ColumnDefinition() { columnDefinitions.add(columnDefinition); } )* + ")" { loadDataStatement.setColumnDefinitions(columnDefinitions); } + ] + [ + LOOKAHEAD({ isKeywordAhead("OPTIONS") }) + "(" options=ExpressionList() ")" { loadDataStatement.setOptions(options); } + ] + + LOOKAHEAD({ isKeywordAhead("FILES") }) + "(" options=ExpressionList() ")" { loadDataStatement.setFileOptions(options); } + [ + LOOKAHEAD(2) + { loadDataStatement.setWithPartitionColumns(true); } + ] + [ + LOOKAHEAD({ getToken(1).kind == K_WITH && "CONNECTION".equalsIgnoreCase(getToken(2).image) }) + connectionName=RelObjectNames() + { loadDataStatement.setConnectionName(String.join(".", connectionName.getNames())); } + ] + { return loadDataStatement; } +} + +/** + * BigQuery's EXPORT DATA [WITH CONNECTION connection] OPTIONS (option_list) AS query. + */ +ExportDataStatement ExportDataStatement() #ExportDataStatement: +{ + ExportDataStatement exportDataStatement = new ExportDataStatement(); + ExpressionList options; + ObjectNames connectionName; + Select select; +} +{ + + [ + LOOKAHEAD({ getToken(1).kind == K_WITH && "CONNECTION".equalsIgnoreCase(getToken(2).image) }) + connectionName=RelObjectNames() + { exportDataStatement.setConnectionName(String.join(".", connectionName.getNames())); } + ] + [ + LOOKAHEAD({ isKeywordAhead("OPTIONS") }) + "(" options=ExpressionList() ")" + { exportDataStatement.setOptions(options); } + ] + select=Select() { exportDataStatement.setSelect(select); } + { return exportDataStatement; } +} + +/** + * BigQuery's ASSERT expression [AS description] debugging statement. + */ +AssertStatement AssertStatement() #AssertStatement: +{ + AssertStatement assertStatement = new AssertStatement(); + Expression expression; + Token token; +} +{ + expression=Expression() { assertStatement.setExpression(expression); } + [ token= + { assertStatement.setDescription(stringValue(token)); } ] + { return assertStatement; } +} + +PurgeStatement PurgeStatement(): { + PurgeStatement purgeStatement = null; + Table table; + Index index; + Token tableSpaceToken; + Token userToken = null; +} +{ + + ( + table=Table() { purgeStatement = new PurgeStatement(table); } + | + index=Index() { purgeStatement = new PurgeStatement(index); } + | + { purgeStatement = new PurgeStatement(PurgeObjectType.RECYCLEBIN); } + | + { purgeStatement = new PurgeStatement(PurgeObjectType.DBA_RECYCLEBIN); } + | + tableSpaceToken= [ userToken= ] { + purgeStatement = new PurgeStatement( + PurgeObjectType.TABLESPACE + , tableSpaceToken.image + , userToken!=null ? userToken.image : null); + } + ) + + { + return purgeStatement; + } +} + +DescribeStatement Describe(): { + Table table; + Select select; + DescribeStatement stmt = new DescribeStatement(); + Token tk = null; +} { + (tk= | tk=) + ( + // DuckDB describes the result of a query, not only a table + LOOKAHEAD({ getToken(1).kind == K_SELECT || getToken(1).kind == K_WITH + || getToken(1).kind == K_FROM || getToken(1).kind == OPENING_BRACKET }) + select = Select() { stmt.setDescribeType(tk.image).setSelect(select); } + | + table = Table() { stmt.setDescribeType(tk.image).setTable(table); } + ) + { + return stmt; + } +} + +ExplainStatement Explain(): +{ + Token tk; + Statement statement; + List> with = null; + Table table; + List options; + ExplainStatement es; + boolean parenthesized = false; + Token statementFirst; +} +{ + ( tk= | tk = ) + ( + LOOKAHEAD(3)( + ( + LOOKAHEAD(2) "(" options = PostgresqlExplainOptions() ")" { parenthesized = true; } + | options = ExplainStatementOptions() + ) + { statementFirst = getToken(1); } + ( + [ LOOKAHEAD(2) with=WithList() ] + ( + statement = SelectWithWithItems( with ) { if (with != null) { widen(statement, statementFirst, null); } } + | statement = InsertWithWithItems( with ) + | statement = UpdateWithWithItems( with ) + | statement = DeleteWithWithItems( with ) + | statement = Merge( with ) + ) + ) + { + linkStatement(statement, statementFirst); + es = new ExplainStatement(tk.image, statement, options); + es.setParenthesizedOptions(parenthesized); + } + ) + | + ( + table=Table( ) { es = new ExplainStatement(tk.image, table); } + ) + ) + { + if (parenthesized || Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { + validatePostgresqlExplain(es); + } + return es; + } +} + +List PostgresqlExplainOptions(): +{ + List options = new ArrayList(); + ExplainStatement.Option option; +} +{ + option = PostgresqlExplainOption() { options.add(option); } + ( "," option = PostgresqlExplainOption() { options.add(option); } )* + { return options; } +} + +ExplainStatement.Option PostgresqlExplainOption(): +{ + Token name; + Token value = null; +} +{ + ( name = | name = | name = + | name = | name = | name = + | name = ) + [ ( value = | value = | value = | value = + | value = | value = | value = | value = + | value = | value = | value = | value = ) ] + { + return new ExplainStatement.Option(postgresqlExplainOptionType(name.image)) + .withValue(value == null ? null : value.image); + } +} + +/** + * Boolean values in the legacy unparenthesized syntax. + */ +String ExplainOptionBoolean(): +{ + Token tk = null; +} +{ + [( tk= | tk= | tk= | tk= )] + { + return tk != null ? tk.image : null; + } +} + +/** + * Preserve the optional format in the legacy unparenthesized syntax. + */ +String ExplainFormatOption(): +{ + Token tk = null; +} +{ + [( tk= | tk= | tk= )] + { + return tk != null ? tk.image : null; + } +} + +/** + * Options for explain, see https://www.postgresql.org/docs/9.1/sql-explain.html + */ +List ExplainStatementOptions(): +{ + List options = new ArrayList(); + ExplainStatement.Option option = null; + Token token = null; + String value = null; +} +{ + ( + ( value=ExplainOptionBoolean() + { + option = new ExplainStatement.Option(ExplainStatement.OptionType.ANALYZE); + option.setValue(value); + options.add(option); + } + ) + | + ( value=ExplainOptionBoolean() + { + option = new ExplainStatement.Option(ExplainStatement.OptionType.BUFFERS); + option.setValue(value); + options.add(option); + } + ) + | + ( value=ExplainOptionBoolean() + { + option = new ExplainStatement.Option(ExplainStatement.OptionType.COSTS); + option.setValue(value); + options.add(option); + } + ) + | + ( value=ExplainOptionBoolean() + { + option = new ExplainStatement.Option(ExplainStatement.OptionType.VERBOSE); + option.setValue(value); + options.add(option); + } + ) + | + ( value=ExplainFormatOption() + { + option = new ExplainStatement.Option(ExplainStatement.OptionType.FORMAT); + option.setValue(value); + options.add(option); + } + ) + | + ( + { option = new ExplainStatement.Option(ExplainStatement.OptionType.PLAN); } + [ { option = new ExplainStatement.Option(ExplainStatement.OptionType.PLAN_FOR); } ] + + value=ExplainFormatOption() + { + option.setValue(value); + options.add(option); + } + ) + )* //zero or many times those productions + { + return options; + } +} + +SetIdentityInsertStatement SetIdentityInsert(): { + Table table; + boolean on; +} +{ + AccessKeyword("IDENTITY_INSERT") table=Table() + { + requireAccessSyntax(table.getNameParts().size() <= 3, + "IDENTITY_INSERT requires a table name with at most three parts"); + } + ( { on = true; } | { on = false; } ) + { + return new SetIdentityInsertStatement(table, on); + } +} + +UseStatement Use(): { + String name; + boolean hasSchemaKeyword = false; +} +{ + [ LOOKAHEAD(2) { hasSchemaKeyword = true; } ] name = RelObjectName() + { + return new UseStatement(name, hasSchemaKeyword); + } +} + +Statement Show(): +{ + Statement statement; + List captureRest; +} +{ + + ( + // A typed form must span the whole statement; other dialects' trailing clauses + // (e.g. Snowflake IN SCHEMA s, ClickHouse FORMAT f) still fall back to captureRest(). + LOOKAHEAD(ShowColumns() ShowStatementEnd()) statement = ShowColumns() + | + LOOKAHEAD(2) statement = ShowIndex() + | + LOOKAHEAD(2) statement = ShowTables() + | + LOOKAHEAD(ShowTableStatus() ShowStatementEnd(), + { getToken(1).kind == K_TABLE && "STATUS".equalsIgnoreCase(getToken(2).image) }) + statement = ShowTableStatus() + | + LOOKAHEAD(ShowCreate() ShowStatementEnd()) statement = ShowCreate() + | + // any of the RDBMS specific SHOW syntax + captureRest = captureRest() + { + if (captureRest.size()==1) { + statement = new ShowStatement(captureRest.get(0)); + } else { + statement = new UnsupportedStatement("SHOW", captureRest); + } + } + ) + { + return statement; + } +} + +/** + * Only used inside the SHOW lookaheads: a typed SHOW statement ends at a semicolon, the input end, + * the ELSE or END of an enclosing IF or block, or the start of the next statement, as in + * Statements(). + */ +void ShowStatementEnd(): +{} +{ + + | + + | + + | + + | + LOOKAHEAD({ getToken(1).kind != K_FROM && getToken(1).kind != K_PIVOT + && getToken(1).kind != K_UNPIVOT && isStatementStartAhead() }) {} +} + +// https://dev.mysql.com/doc/refman/8.4/en/show-columns.html +ShowColumnsStatement ShowColumns(): { + ShowColumnsStatement statement = new ShowColumnsStatement(); + Table table; + String dbName; + Expression expression; +} +{ + [ { statement.setExtended(true); } ] + [ { statement.setFull(true); } ] + ( | { statement.setFieldsKeyword(true); } ) + ( + + | + { statement.setTableSelectionMode(ShowTablesStatement.SelectionMode.IN); } + ) + table = Table() { statement.setTable(table); } + [ + LOOKAHEAD(2) + ( + + | + { statement.setDbSelectionMode(ShowTablesStatement.SelectionMode.IN); } + ) + dbName = RelObjectName() { statement.setDbName(dbName); } + ] + [ + expression = SimpleExpression() { statement.setLikeExpression(expression); } + | + expression = Expression() { statement.setWhereCondition(expression); } + ] + { + return statement; + } +} + +// https://dev.mysql.com/doc/refman/8.4/en/show-table-status.html +ShowTableStatusStatement ShowTableStatus(): { + ShowTableStatusStatement statement = new ShowTableStatusStatement(); + String dbName; + Expression expression; +} +{ + ContextualKeyword("STATUS") + [ + LOOKAHEAD(2) + ( + + | + { statement.setSelectionMode(ShowTablesStatement.SelectionMode.IN); } + ) + dbName = RelObjectName() { statement.setDbName(dbName); } + ] + [ + expression = SimpleExpression() { statement.setLikeExpression(expression); } + | + expression = Expression() { statement.setWhereCondition(expression); } + ] + { + return statement; + } +} + +/** + * MySQL SHOW CREATE {TABLE | VIEW | PROCEDURE | FUNCTION | TRIGGER | EVENT} name, + * SHOW CREATE {DATABASE | SCHEMA} [IF NOT EXISTS] db and SHOW CREATE USER account. + */ +ShowCreateStatement ShowCreate(): { + ShowCreateStatement statement = new ShowCreateStatement(); + Table name; + String databaseName; + String currentUser; + UserAccount account = new UserAccount(); +} +{ + + ( + ( + { statement.setObjectType(ShowCreateStatement.ObjectType.TABLE); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.VIEW); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.PROCEDURE); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.FUNCTION); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.TRIGGER); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.EVENT); } + ) + name = Table() { statement.setName(name); } + | + ( | { statement.setSchemaKeyword(true); } ) + { statement.setObjectType(ShowCreateStatement.ObjectType.DATABASE); } + [ LOOKAHEAD(3) { statement.setIfNotExists(true); } ] + databaseName = RelObjectName() { statement.setDatabaseName(databaseName); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.USER); } + ( + LOOKAHEAD({ isKeywordAhead("CURRENT_USER") }) currentUser = RelObjectName() + { account.setUser(linkToLastToken(new StringValue(currentUser).setQuoteStr(""))); } + [ LOOKAHEAD(2) "(" ")" { statement.setCurrentUserParentheses(true); } ] + | + account = MySqlAccountName() + ) + { statement.setAccount(account); } + ) + { + return statement; + } +} + +ShowIndexStatement ShowIndex(): { + String tableName; +} +{ + tableName = RelObjectName() + { + return new ShowIndexStatement(tableName); + } +} + +Statement RefreshMaterializedView(): { + Table view = null; + boolean concurrently = false; + RefreshMode refreshMode = null; + List captureRest; +} +{ + + [ LOOKAHEAD(2) { concurrently = true; } ] + view = Table() + [ + { refreshMode = RefreshMode.WITH_DATA; } + [ + { refreshMode = RefreshMode.WITH_NO_DATA; } + ] + + ] + captureRest = captureRest() + { + if (concurrently && refreshMode == RefreshMode.WITH_NO_DATA) { + return new UnsupportedStatement("REFRESH", captureRest); + } else { + return new RefreshMaterializedViewStatement(view, concurrently, refreshMode); + } + } +} +// https://dev.mysql.com/doc/refman/8.0/en/show-tables.html +ShowTablesStatement ShowTables(): { + ShowTablesStatement showTablesStatement; + EnumSet modifiers = EnumSet.noneOf(ShowTablesStatement.Modifiers.class); + ShowTablesStatement.SelectionMode selectionMode = null; + String dbName = null; + Expression likeExpression = null; + Expression whereCondition = null; +} +{ + [ { modifiers.add(ShowTablesStatement.Modifiers.EXTENDED); } ] + [ { modifiers.add(ShowTablesStatement.Modifiers.FULL); } ] + + [ + LOOKAHEAD(2) ( + { selectionMode = ShowTablesStatement.SelectionMode.FROM; } + | + { selectionMode = ShowTablesStatement.SelectionMode.IN; } + ) + dbName = RelObjectName() + ] + [ ( likeExpression = SimpleExpression() | whereCondition = Expression()) ] + { + showTablesStatement = new ShowTablesStatement(); + showTablesStatement.setModifiers(modifiers); + showTablesStatement.setSelectionMode(selectionMode); + showTablesStatement.setDbName(dbName); + showTablesStatement.setLikeExpression(likeExpression); + showTablesStatement.setWhereCondition(whereCondition); + return showTablesStatement; + } +} + +Values Values(): { + ExpressionList expressions; + Token first = getToken(1); +} { + ( | ) + expressions = ExpressionList() + + { + Values values = new Values(expressions); + linkAST(values, first); + return values; + } +} + +ReturningClause ReturningClause(): +{ + Token keyword; + List outputAliases = null; + List> selectItems; + Object dataItem; + List dataItems = null; + boolean bulkCollect = false; +} +{ + ( keyword= | keyword= ) + [ LOOKAHEAD(2) outputAliases = ReturningOutputAliasList() ] + selectItems = SelectItemsList() + + [ + LOOKAHEAD({ getToken(1).kind == K_INTO || isBulkCollectIntoAhead() }) + [ LOOKAHEAD({ isBulkCollectIntoAhead() }) + ContextualKeyword("BULK") ContextualKeyword("COLLECT") { bulkCollect = true; } + ] + + dataItem = ReturningIntoTarget() + { dataItems = new ArrayList(); dataItems.add(dataItem); } + + ( + "," + dataItem = ReturningIntoTarget() { dataItems.add(dataItem); } + )* + ] + + { + return new ReturningClause(keyword.image, selectItems, outputAliases, dataItems) + .setBulkCollect(bulkCollect); + } +} + +Object ReturningIntoTarget(): +{ + Object target; +} +{ + ( target=JdbcNamedParameter() | target=JdbcParameter() + | target=Table() | target=UserVariable() ) + { return target; } +} + +ReturningReferenceType ReturningReferenceKind(): +{ + String refName; + ReturningReferenceType refType; +} +{ + refName = RelObjectName() + { + refType = ReturningReferenceType.from(refName); + if (refType == ReturningReferenceType.OLD) { + return ReturningReferenceType.OLD; + } else if (refType == ReturningReferenceType.NEW) { + return ReturningReferenceType.NEW; + } + throw new ParseException("Expected OLD or NEW but found: " + refName); + } +} + +ReturningOutputAlias ReturningOutputAliasDefinition(): +{ + ReturningReferenceType refType; + String aliasName; +} +{ + refType = ReturningReferenceKind() + + aliasName = RelObjectName() + { + return new ReturningOutputAlias(refType, aliasName); + } +} + +List ReturningOutputAliasList(): +{ + List outputAliases = new ArrayList(); + ReturningOutputAlias outputAlias; +} +{ + "(" + outputAlias = ReturningOutputAliasDefinition() { outputAliases.add(outputAlias); } + ( + "," + outputAlias = ReturningOutputAliasDefinition() { outputAliases.add(outputAlias); } + )* + ")" + { + return outputAliases; + } +} + +Update UpdateWithWithItems( List> withItems ): +{ + Update update; +} +{ + update = Update() { update.setWithItemsList( withItems ); + return update; +} +} + +Update Update(): +{ + Update update = new Update(); + Table table = null; + ForPortionClause forPortionClause; + List startJoins = null; + List> with = null; + List updateSets; + Expression where = null; + PreferringClause preferringClause = null; + Limit limit = null; + OptionClause optionClause = null; + List orderByElements; + boolean useColumnsBrackets = false; + ReturningClause returningClause; + Token tk = null; + UpdateModifierPriority modifierPriority = null; + boolean modifierIgnore = false; + + OutputClause outputClause = null; +} +{ + { update.setOracleHint(getOracleHint()); } + [ LOOKAHEAD(2) { modifierPriority = UpdateModifierPriority.LOW_PRIORITY; }] + [ LOOKAHEAD(2) { modifierIgnore = true; }] + table=TableWithAliasAndMysqlIndexHint() + [ forPortionClause=ForPortionClause() { update.setForPortionClause(forPortionClause); } ] + [ startJoins=JoinsList() ] + [ LOOKAHEAD(, { Dialect.TERADATA.name().equals(getAsString(Feature.dialect)) }) + UpdateFromClause(update) { update.setFromBeforeSet(true); } ] + updateSets = UpdateSets() { update.setUpdateSets(updateSets); } + + [ outputClause = OutputClause() {update.setOutputClause(outputClause); } ] + + [ LOOKAHEAD(2, { !Dialect.TERADATA.name().equals(getAsString(Feature.dialect)) }) + UpdateFromClause(update) ] + + [ where=WhereClause() { update.setWhere(where); } ] + [ preferringClause=PreferringClause() { update.setPreferringClause(preferringClause); } ] + + [ orderByElements = OrderByElements() { update.setOrderByElements(orderByElements); } ] + [ limit = PlainLimit() { update.setLimit(limit); } ] + [ LOOKAHEAD(2) optionClause = OptionClause() { update.setOption(optionClause); } ] + [ returningClause = ReturningClause() { update.setReturningClause(returningClause); } ] + + { + return update.withWithItemsList(with) + .withTable(table) + .withStartJoins(startJoins) + .withModifierPriority(modifierPriority) + .withModifierIgnore(modifierIgnore); + } +} + +/** Shared structured interval for application-time UPDATE and DELETE. */ +ForPortionClause ForPortionClause() #ForPortionClause: +{ + String periodName; + Expression fromExpression; + Expression toExpression; + ForPortionClause clause; +} +{ + AccessKeyword("PORTION") periodName=RelObjectName() + fromExpression=Expression() toExpression=Expression() + { + clause = new ForPortionClause(periodName, fromExpression, toExpression); + linkAST(clause, jjtThis); + return clause; + } +} + +void UpdateFromClause(Update update): +{ + FromItem fromItem; + List joins = null; +} +{ + fromItem=FromItem() + [ LOOKAHEAD(2) joins=JoinsList() ] + { update.setFromItem(fromItem); update.setJoins(joins); } +} + +List UpdateSets(): +{ + ArrayList updateSets = new ArrayList(); + UpdateSet updateSet; + Column tableColumn; + Expression valueExpression; + + ExpressionList columns; + ExpressionListvalues; +} +{ + ( + ( + tableColumn=Column() "=" valueExpression=Expression() + { + updateSet = new UpdateSet(tableColumn, valueExpression); + linkElements(updateSet.getColumns()); + linkElements(updateSet.getValues()); + updateSets.add(updateSet); + } + ) + | + ( + { updateSet = new UpdateSet(); updateSets.add(updateSet); } + columns = ParenthesedExpressionList() { updateSet.setColumns(columns); } + "=" + ( + LOOKAHEAD(3) valueExpression = ParenthesedSelect() { updateSet.setValues(linkElements(new ExpressionList(valueExpression))); } + | + values = ParenthesedExpressionList() { updateSet.setValues(values); } + ) + ) + ) + + ( + LOOKAHEAD(2) "," ( + tableColumn=Column() "=" valueExpression=Expression() + { + updateSet = new UpdateSet(tableColumn, valueExpression); + linkElements(updateSet.getColumns()); + linkElements(updateSet.getValues()); + updateSets.add(updateSet); + } + | + ( + { updateSet = new UpdateSet(); updateSets.add(updateSet); } + columns = ParenthesedExpressionList() { updateSet.setColumns(columns); } + "=" + ( + LOOKAHEAD(3) valueExpression = ParenthesedSelect() { updateSet.setValues(linkElements(new ExpressionList(valueExpression))); } + | + values = ParenthesedExpressionList() { updateSet.setValues(values); } + ) + ) + ) + )* + + { + return updateSets; + } +} + +List Partitions(): +{ + List partitions = new ArrayList(); + Column tableColumn; + Expression valueExpression = null; +} +{ + ( + ( + tableColumn=Column() [ "=" valueExpression=Expression() ] + { partitions.add( new Partition (tableColumn, valueExpression)); } + ) + ) + + ( + LOOKAHEAD(2) ( + "," + tableColumn=Column() { valueExpression = null; } [ "=" valueExpression=Expression() ] + { partitions.add( new Partition (tableColumn, valueExpression)); } + ) + )* + + { + return partitions; + } +} + +InsertBulk InsertBulk(): +{ + InsertBulk statement = new InsertBulk(); + Table table; + ColumnDefinition column; + InsertBulk.Option option; + Set kinds = EnumSet.noneOf(InsertBulk.Option.Kind.class); +} +{ + AccessKeyword("BULK") table=Table() { statement.setTable(table); } + "(" column=InsertBulkColumn() { statement.getColumns().add(column); } + ( "," column=InsertBulkColumn() { statement.getColumns().add(column); } )* ")" + // LOOKAHEAD(2) is required, not cosmetic: Statements() permits two statements to be + // adjacent without a separator, so FIRST(SingleStatement) -- including -- is in + // the FOLLOW set of every statement tail. `WITH (` cannot start a CTE, so two tokens + // separate the INSERT BULK option list from a following WITH statement. + [ LOOKAHEAD(2) "(" option=InsertBulkOption() { + statement.getOptions().add(option); kinds.add(option.getKind()); + } + ( "," option=InsertBulkOption() { + requireDdlSyntax(kinds.add(option.getKind()), "Duplicate INSERT BULK option"); + statement.getOptions().add(option); + } )* ")" ] + { return statement; } +} + +ColumnDefinition InsertBulkColumn(): +{ + String name; + String collation; + ColDataType type; + ColumnDefinition column; +} +{ + name=RelObjectName() type=ColDataType() { column = new ColumnDefinition(name, type); } + [ collation=RelObjectName() { column.setColumnSpecs(Arrays.asList("COLLATE", collation)); } ] + { return column; } +} + +InsertBulk.Option InsertBulkOption(): +{ + String name; + Token value; + Column column; + Token direction; + OrderByElement order; + InsertBulk.Option option; +} +{ + ( + // RelObjectName() below accepts as an identifier (isReservedKeywordAsIdentifier() + // returns true for ORDER unless BY or SIBLINGS follows), so ORDER is a genuine common + // prefix of the first and the last alternative. The option form always has "(" after it. + LOOKAHEAD( "(") + { option = new InsertBulk.Option(InsertBulk.Option.Kind.ORDER); } + "(" column=Column() { + order = new OrderByElement().withExpression(column); + option.getOrderByElements().add(order); + } + [ (direction= | direction=) { order.setAsc(direction.kind == K_ASC); order.setAscDescPresent(true); } ] + { linkAST(order, firstToken(column, getToken(0))); } + ( "," column=Column() { + order = new OrderByElement().withExpression(column); + option.getOrderByElements().add(order); + } + [ (direction= | direction=) { order.setAsc(direction.kind == K_ASC); order.setAscDescPresent(true); } ] + { linkAST(order, firstToken(column, getToken(0))); } + )* ")" + | + LOOKAHEAD({ isKeywordAhead("ROWS_PER_BATCH") }) AccessKeyword("ROWS_PER_BATCH") "=" value= + { option = new InsertBulk.Option(InsertBulk.Option.Kind.ROWS_PER_BATCH); option.setValue(new LongValue(value.image)); } + | + name=RelObjectName() { + option = new InsertBulk.Option(typeDdlEnum(InsertBulk.Option.Kind.class, name)); + requireDdlSyntax(option.getKind() != InsertBulk.Option.Kind.ORDER + && option.getKind() != InsertBulk.Option.Kind.ROWS_PER_BATCH, "Missing INSERT BULK option value"); + } + ) + { return option; } +} + +Insert InsertWithWithItems( List> withItems ): +{ + Insert insert; +} +{ + insert = Insert() { insert.setWithItemsList( withItems ); + return insert; +} +} + +Insert Insert(): +{ + Insert insert = new Insert(); + Table table = null; + List> with = null; + Column tableColumn = null; + ExpressionList columns = new ExpressionList(); + List partitions = new ArrayList(); + Expression exp = null; + ReturningClause returningClause; + Select select = null; + Token tk = null; + InsertModifierPriority modifierPriority = null; + boolean modifierIgnore = false; + + List updateSets; + List duplicateUpdateSets; + + String name = null; + boolean useAs = false; + boolean useSet = false; + Alias rowAlias = null; + OutputClause outputClause = null; + + InsertConflictTarget conflictTarget = null; + InsertConflictAction conflictAction = null; + + InsertDuplicateAction duplicateAction = null; + Token multiInsertToken = null; + List oracleMultiInsertBranches = new ArrayList(); + OracleMultiInsertClause oracleMultiInsertClause = null; + OracleMultiInsertBranch oracleMultiInsertBranch = null; +} +{ + { insert.setOracleHint(getOracleHint()); } + + [ + LOOKAHEAD(2) (tk = | tk = | tk = ) + { + if (tk!=null) + modifierPriority = InsertModifierPriority.from(tk.image); + } + ] + [ LOOKAHEAD(2) { modifierIgnore = true; }] + ( + LOOKAHEAD(2, ( | ) ( | )) + ( + (multiInsertToken = | multiInsertToken = ) { + insert.setOracleMultiInsert(true); + insert.setOracleMultiInsertFirst(multiInsertToken.kind == K_FIRST); + } + ( + { + oracleMultiInsertBranch = new OracleMultiInsertBranch(); + } + oracleMultiInsertClause = OracleMultiInsertClause() { + oracleMultiInsertBranch.addClause(oracleMultiInsertClause); + table = oracleMultiInsertClause.getTable(); + } + ( + oracleMultiInsertClause = OracleMultiInsertClause() { + oracleMultiInsertBranch.addClause(oracleMultiInsertClause); + } + )* + { + oracleMultiInsertBranches.add(oracleMultiInsertBranch); + } + | + ( + ( + oracleMultiInsertBranch = OracleMultiInsertWhenBranch() { + if (table == null && !oracleMultiInsertBranch.getClauses().isEmpty()) { + table = oracleMultiInsertBranch.getClauses().get(0).getTable(); + } + oracleMultiInsertBranches.add(oracleMultiInsertBranch); + } + )+ + [ + oracleMultiInsertBranch = OracleMultiInsertElseBranch() { + if (table == null && !oracleMultiInsertBranch.getClauses().isEmpty()) { + table = oracleMultiInsertBranch.getClauses().get(0).getTable(); + } + oracleMultiInsertBranches.add(oracleMultiInsertBranch); + } + ] + ) + ) + select = Select() { + insert.setOracleMultiInsertBranches(oracleMultiInsertBranches); + } + ) + | + ( + [ LOOKAHEAD(2) ( + { insert.setOverwrite(true); insert.setTableKeyword(true); } + | [ LOOKAHEAD(2) { insert.setTableKeyword(true); }] + ) + ] table=TableReference() + [ LOOKAHEAD(2) "(" partitions=Partitions() ")" ] + + [ LOOKAHEAD({ isAliasAhead() && !isColumnMatchingAhead() + && !(getToken(1).kind == K_OVERRIDING + && (getToken(2).kind == K_SYSTEM || getToken(2).kind == K_USER)) }) + [ { useAs = true; } ] name=RelObjectName() { table.setAlias(new Alias(name,useAs)); }] + + [ LOOKAHEAD(2) "(" columns=ColumnList() ")" ] + + // DuckDB: INSERT INTO tbl BY NAME|POSITION SELECT ... + [ + LOOKAHEAD({ isColumnMatchingAhead() }) + + ( + { insert.setColumnMatching(Insert.ColumnMatching.BY_NAME); } + | + // POSITION is lexed as a string-function name + + { insert.setColumnMatching(Insert.ColumnMatching.BY_POSITION); } + ) + ] + + [ LOOKAHEAD(2) + ( { insert.setOverridingMode(Insert.OverridingMode.SYSTEM); } + | { insert.setOverridingMode(Insert.OverridingMode.USER); } ) ] + + [ outputClause = OutputClause() { insert.setOutputClause(outputClause); } ] + + ( + { insert.setOnlyDefaultValues(true); } + | + ( + updateSets = UpdateSets() { insert.withSetUpdateSets(updateSets); useSet = true; } + ) + | + select = Select() + ) + + [ LOOKAHEAD({ isAliasAhead() && (select instanceof Values || useSet) }) rowAlias = Alias() { + if (select instanceof Values) { + select.setAlias(rowAlias); + } else { + insert.setRowAlias(rowAlias); + } + } ] + + [ LOOKAHEAD(2) + duplicateAction = InsertDuplicateAction() { insert.setDuplicateAction(duplicateAction); } + ] + + [ + + [ conflictTarget = InsertConflictTarget() ] + conflictAction = InsertConflictAction() { insert.withConflictTarget(conflictTarget).setConflictAction(conflictAction); } + ] + + [ returningClause = ReturningClause() { insert.setReturningClause(returningClause); } ] + ) + ) + + { + if (!columns.isEmpty()) { + insert.setColumns(columns); + } + if (!partitions.isEmpty()) { + insert.setPartitions(partitions); + } + return insert.withWithItemsList(with) + .withSelect(select) + .withTable(table) + .withModifierPriority(modifierPriority) + .withModifierIgnore(modifierIgnore); + } +} + +OracleMultiInsertClause OracleMultiInsertClause(): +{ + OracleMultiInsertClause clause = new OracleMultiInsertClause(); + Table clauseTable = null; + ExpressionList clauseColumns = new ExpressionList(); + Select clauseSelect = null; +} +{ + clauseTable=Table() + [ LOOKAHEAD(2) "(" clauseColumns=ColumnList() ")" ] + clauseSelect = Select() + { + if (!clauseColumns.isEmpty()) { + clause.setColumns(clauseColumns); + } + return clause.withTable(clauseTable).withSelect(clauseSelect); + } +} + +OracleMultiInsertBranch OracleMultiInsertWhenBranch(): +{ + Expression whenExpression = null; + List clauses = null; +} +{ + whenExpression = Expression() + clauses = OracleMultiInsertClauseList() + { + return new OracleMultiInsertBranch() + .withWhenExpression(whenExpression) + .withClauses(clauses); + } +} + +OracleMultiInsertBranch OracleMultiInsertElseBranch(): +{ + List clauses = null; +} +{ + + clauses = OracleMultiInsertClauseList() + { + return new OracleMultiInsertBranch() + .withElseClause(true) + .withClauses(clauses); + } +} + +List OracleMultiInsertClauseList(): +{ + List clauses = new ArrayList(); + OracleMultiInsertClause clause = null; +} +{ + clause = OracleMultiInsertClause() { + clauses.add(clause); + } + ( + clause = OracleMultiInsertClause() { + clauses.add(clause); + } + )* + { + return clauses; + } +} + +InsertConflictTarget InsertConflictTarget(): +{ + InsertConflictTarget target = new InsertConflictTarget(); + List elements = new ArrayList(); + KeyElement element; + Expression whereExpression; + String constraintName; +} +{ + ( + "(" element=InsertConflictIndexElement() { elements.add(element); } + ( "," element=InsertConflictIndexElement() { elements.add(element); } )* ")" + { target.setIndexElements(elements); } + [ whereExpression=WhereClause() { target.setWhereExpression(whereExpression); } ] + | + constraintName=RelObjectNameExt() + { target.setConstraintName(constraintName); } + ) + { return target; } +} + +KeyElement InsertConflictIndexElement(): +{ + KeyElement element; + String name; + Expression expression; + ObjectNames collation; + ObjectNames operatorClass; +} +{ + ( + LOOKAHEAD({ isFunctionAhead() }) expression=Function() + { element = new KeyElement(expression).withExpressionParenthesized(false); } + | name=RelObjectNameExt() { element = new KeyElement(name); } + | "(" expression=Expression() ")" { element = new KeyElement(expression); } + ) + [ LOOKAHEAD(2) collation=RelObjectNames() + { element.setCollation(createTable(collation.getNames()).getFullyQualifiedName()); } ] + [ LOOKAHEAD(2) operatorClass=RelObjectNames() { element.setOperatorClass(createTable(operatorClass.getNames()).getFullyQualifiedName()); } ] + { return element; } +} + +InsertConflictAction InsertConflictAction(): +{ + InsertConflictAction conflictAction; + Expression whereExpression = null; + List updateSets; +} +{ + ( + LOOKAHEAD(2) ( + { conflictAction = new InsertConflictAction( ConflictActionType.DO_NOTHING ); } + ) + | + ( + { conflictAction = new InsertConflictAction( ConflictActionType.DO_UPDATE ); } + updateSets = UpdateSets() { conflictAction.setUpdateSets(updateSets); } + [ whereExpression = WhereClause() ] + ) + ) + + { return conflictAction + .withWhereExpression(whereExpression); } +} + +InsertDuplicateAction InsertDuplicateAction(): +{ + InsertDuplicateAction duplicateAction; + Expression whereExpression = null; + List updateSets; +} +{ + ( + LOOKAHEAD(2) ( + { duplicateAction = new InsertDuplicateAction( ConflictActionType.NOTHING ); } + ) + | + ( + { duplicateAction = new InsertDuplicateAction( ConflictActionType.DO_UPDATE ); } + updateSets = UpdateSets() { duplicateAction.setUpdateSets(updateSets); } + [ whereExpression = WhereClause() ] + ) + ) + + { return duplicateAction + .withWhereExpression(whereExpression); } +} + + +OutputClause OutputClause(): +{ + List> selectItemList = null; + UserVariable tableVariable = null; + Table outputTable = null; + List columnList = null; +} +{ + + selectItemList = SelectItemsList() + [ + ( + tableVariable = UserVariable() + | + outputTable = Table() + ) + [ + LOOKAHEAD(2) columnList = ColumnsNamesList() + ] + ] + + { + return new OutputClause(selectItemList, tableVariable, outputTable, columnList); + } +} + +Upsert Upsert(): +{ + Upsert upsert = new Upsert(); + String algorithm; + Table table = null; + ExpressionList columns; + List updateSets; + + Select select = null; + List duplicateUpdateSets; + InsertDuplicateAction duplicateAction = null; + Token tk = null; +} +{ + ( + { upsert.setUpsertType(UpsertType.UPSERT); } + | + { upsert.setUpsertType(UpsertType.REPLACE); } + | + ( + + // ABORT and FAIL are not keywords of the parser + algorithm=RelObjectName() + { upsert.setUpsertType(insertOrType(algorithm)); } + ) + ) + [ LOOKAHEAD(2) { upsert.setUsingInto(true); } ] + + table=Table() { upsert.setTable(table); } + + [ LOOKAHEAD(2) columns = ParenthesedColumnList() { upsert.setColumns(columns); } ] + ( + ( + + updateSets = UpdateSets() { upsert.setUpdateSets(updateSets); } + ) + | + ( + select = Select() { upsert.setSelect(select); } + ) + ) + + [ + + duplicateAction = InsertDuplicateAction() { upsert.setDuplicateAction(duplicateAction); } + ] + + { + return upsert; + } +} + +Delete DeleteWithWithItems( List> withItems ): +{ + Delete delete; +} +{ + delete = Delete() { delete.setWithItemsList( withItems ); + return delete; +} +} + +Delete Delete(): +{ + Delete delete = new Delete(); + Table table = null; + ForPortionClause forPortionClause; + List

tables = new ArrayList
(); + List> with = null; + FromItem usingFromItem = null; + List usingFromItemList = new ArrayList(); + List joins = null; + Expression where = null; + PreferringClause preferringClause = null; + Limit limit = null; + OptionClause optionClause = null; + List orderByElements; + boolean hasFrom = false; + Token tk = null; + DeleteModifierPriority modifierPriority = null; + boolean modifierIgnore = false; + boolean modifierQuick = false; + + ReturningClause returningClause; + OutputClause outputClause; +} +{ + { delete.setOracleHint(getOracleHint()); } + [ LOOKAHEAD(2) { modifierPriority = DeleteModifierPriority.LOW_PRIORITY; }] + [ LOOKAHEAD(2) { modifierQuick = true; }] + [ LOOKAHEAD(2) { modifierIgnore = true; }] + [LOOKAHEAD(4) (table=TableWithAlias() { tables.add(table); } + ("," table=TableWithAlias() { tables.add(table); } )* + [ outputClause = OutputClause() {delete.setOutputClause(outputClause); } ] + | ) { hasFrom = true; }] + + [ LOOKAHEAD(3) table=TableWithAlias() + [ forPortionClause=ForPortionClause() { delete.setForPortionClause(forPortionClause); } ] + [ LOOKAHEAD(2) joins=JoinsList() ] ] + [ usingFromItem=FromItem() { usingFromItemList.add(usingFromItem); } + ("," usingFromItem=FromItem() { usingFromItemList.add(usingFromItem); } )*] + [where=WhereClause() { delete.setWhere(where); } ] + [preferringClause=PreferringClause() { delete.setPreferringClause(preferringClause);} ] + [orderByElements = OrderByElements() { delete.setOrderByElements(orderByElements); } ] + [limit=PlainLimit() {delete.setLimit(limit); } ] + [ LOOKAHEAD(2) optionClause = OptionClause() { delete.setOption(optionClause); } ] + + [ returningClause = ReturningClause() { delete.setReturningClause(returningClause); } ] + { + if (joins != null && joins.size() > 0) { + delete.setJoins(joins); + } + requireDeleteSyntax(table != null || !tables.isEmpty(), + "DELETE requires a target table"); + return delete.withWithItemsList(with) + .withTables(tables) + .withTable(table) + .withHasFrom(hasFrom) + .withUsingFromItemList(usingFromItemList) + .withModifierPriority(modifierPriority) + .withModifierIgnore(modifierIgnore) + .withModifierQuick(modifierQuick); + } +} + +Statement Merge( List> with ) : { + Merge merge = new Merge(); + Table table; + FromItem fromItem; + Expression condition; + List operations; + OutputClause outputClause; + ReturningClause returningClause; + OptionClause optionClause = null; +} +{ + { merge.setOracleHint(getOracleHint()); } table=TableWithAlias() { merge.setTable(table); } + fromItem = FromItem() { merge.setFromItem(fromItem); } + condition = Expression() { merge.setOnCondition(condition); } + + operations = MergeOperations() { merge.setOperations(operations); } + + [ returningClause = ReturningClause() { merge.setReturningClause(returningClause); } ] + + [ outputClause = OutputClause() { merge.setOutputClause(outputClause); } ] + + [ LOOKAHEAD(2) optionClause = OptionClause() { merge.setOption(optionClause); } ] + + { return merge.withWithItemsList(with); } +} + +List MergeOperations() : { + List operationsList = new ArrayList(); + MergeOperation operation; +} +{ + ( + LOOKAHEAD(2) operation = MergeWhenMatched() { operationsList.add(operation); } + | + operation = MergeWhenNotMatched() { operationsList.add(operation); } + )* + { return operationsList; } +} + +MergeOperation MergeWhenMatched() : { + Expression predicate = null; + MergeOperation operation; +} +{ + + [ predicate = Expression() ] + + ( + operation = MergeDeleteClause(predicate) | operation = MergeUpdateClause(predicate) + ) + { return operation; } +} + +MergeOperation MergeDeleteClause(Expression predicate) : { + MergeDelete md = new MergeDelete().withAndPredicate(predicate); +} +{ + { return md; } +} + +MergeOperation MergeUpdateClause(Expression predicate) : { + MergeUpdate mu = new MergeUpdate().withAndPredicate(predicate); + List updateSets; + Expression condition; +} +{ + updateSets = UpdateSets() { mu.setUpdateSets(updateSets); } + [ condition = Expression() { mu.setWhereCondition(condition); }] + [ LOOKAHEAD(2) condition = Expression() { mu.setDeleteWhereCondition(condition); } ] + { return mu; } +} + +MergeInsert MergeInsertClause(Expression predicate) : { + MergeInsert mi = new MergeInsert(); + ExpressionList columns; + ExpressionList expList; + Expression condition; + Token open; +} +{ + + [ open="(" columns = ColumnList() ")" + { + ParenthesedExpressionList insertColumns = new ParenthesedExpressionList(columns); + linkAST(insertColumns, open); + mi.setColumns(insertColumns); + } + ] + open="(" expList = SimpleExpressionList() ")" + { + ParenthesedExpressionList insertValues = new ParenthesedExpressionList(expList); + linkAST(insertValues, open); + mi.setValues(insertValues); + } + + [ condition = Expression() { mi.setWhereCondition(condition); } ] + + { + mi.setAndPredicate(predicate); + return mi; + } +} + +MergeOperation MergeWhenNotMatched() : { + MergeOperation operation; + Expression predicate = null; + MergeSide side = null; + Token sideToken; +} +{ + + [ sideToken = { + side = MergeSide.fromImage(sideToken.image); + } ] + [ predicate = Expression() ] + + ( + operation = MergeInsertClause(predicate) { ((MergeInsert) operation).setSide(side); } + | + operation = MergeUpdateClause(predicate) { ((MergeUpdate) operation).setSide(side); } + | + operation = MergeDeleteClause(predicate) { ((MergeDelete) operation).setSide(side); } + ) + { return MergeSide.validatePairing(side, operation); } +} + +// table names seem to allow ":" delimiters, e.g. for Informix see #1134 +ObjectNames RelObjectNames() : { + String token = null; + Token delimiter = null; + List data = new ArrayList(); + List delimiters = new ArrayList(); +} { + token = RelObjectName() { data.add(token); } + ( + LOOKAHEAD (2) ( + ( delimiter = "..." { delimiters.add("."); data.add(null); delimiters.add("."); data.add(null); delimiters.add("."); } ) + | + ( delimiter = ".." { delimiters.add("."); data.add(null); delimiters.add("."); } ) + | + ( ( delimiter = "." | delimiter = ":" ) { delimiters.add(delimiter.image); } ) + ) + + token = RelObjectNameExt() { data.add(token); } + ) * + + { return new ObjectNames(data, delimiters); } +} + +// column names do not allow ":" delimeters as those represent JSON `GET` operators +ObjectNames ColumnIdentifier() : { + String token = null; + Token delimiter = null; + List data = new ArrayList(); + List delimiters = new ArrayList(); +} { + token = RelObjectName() { data.add(token); } + ( + LOOKAHEAD (2) ( + ( delimiter = "..." { delimiters.add("."); data.add(null); delimiters.add("."); data.add(null); delimiters.add("."); } ) + | + ( delimiter = ".." { delimiters.add("."); data.add(null); delimiters.add("."); } ) + | + ( delimiter = "." { delimiters.add(delimiter.image); } ) + ) + + token = RelObjectNameExt() { data.add(token); } + ) * + + { return new ObjectNames(data, delimiters); } +} + +// See: http://technet.microsoft.com/en-us/library/ms187879%28v=sql.105%29.aspx +Column Column() #Column : +{ + ObjectNames data = null; + ArrayConstructor arrayConstructor = null; + Token tk = null; + Token first = getToken(1); + Token name; +} +{ + data = ColumnIdentifier() { name = getToken(0); } + [ LOOKAHEAD(2) tk= ] + // @todo: we better should return a SEQUENCE instead of a COLUMN + [ LOOKAHEAD(2) "." { data.getNames().add("nextval"); name = getToken(0); } ] + + [ LOOKAHEAD(2) arrayConstructor = Subscript() ] + { + Column col = createColumn(data.getNames(), data.getDelimiters()); + if (tk != null) { col.withCommentText(tk.image); } + if (arrayConstructor!=null) { + col.setArrayConstructor(arrayConstructor); + } + Token qualifierLast = qualifierLast(first, name); + if (col.getTable() != null && qualifierLast != null) { + linkAST(col.getTable(), first, qualifierLast); + } + linkAST(col,jjtThis); + return col; + } +} + +/* + * Unified identifier production for SQL object names. + * + * Accepts base identifier tokens (S_IDENTIFIER, S_QUOTED_IDENTIFIER, DATA_TYPE, + * date literals), any non-reserved keyword, and — guarded by the semantic + * check isReservedKeywordAsIdentifier() — reserved keywords that can serve as + * unquoted identifiers in the current parser position. + * + * NOTE: K_FROM and K_SELECT are deliberately NOT included here + * to avoid FIRST-set pollution in JavaCC's choice resolution. They are only + * valid as identifiers in dotted-name continuations (after '.'), named + * parameters (after ':'), index columns, constraint names, and similar + * restricted contexts. Those call sites add the two tokens inline. + */ +String RelObjectName() : +{ Token tk = null; String result = null; } +{ + ( + /* ── Base identifier tokens ─────────────────────────────────────── */ + tk= + | tk= + | tk= + | tk= + | tk= + + /* ── Non-reserved keywords (range-guarded) ──────────────────────── */ + | LOOKAHEAD({ getToken(1).kind >= MIN_NON_RESERVED_WORD + && getToken(1).kind <= MAX_NON_RESERVED_WORD }) + result = NonReservedWord() + + /* ── Reserved keywords usable as identifiers (context-guarded) ─── */ + | LOOKAHEAD({ isReservedKeywordAsIdentifier() }) + ( tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= ) + ) + { return result != null ? result : tk.image; } +} + +/* + * Extended version of RelObjectName that additionally accepts FROM and SELECT + * as unquoted identifiers. + * + * These two tokens are kept OUT of RelObjectName() to avoid polluting + * JavaCC's FIRST-set computation (which would cause choice conflicts in + * expression parsing, subselect detection, etc.). + * + * Used only in specific contexts where the old grammar's RelObjectNameExt2 + * was called: dotted-name continuations, index/constraint names, type names, + * named-parameter identifier chains, and join table references. + */ +String RelObjectNameExt() : +{ Token tk = null; String result = null; } +{ + ( result = RelObjectName() | tk= | tk= ) + { return tk != null ? tk.image : result; } +} + +Table Table() #TableName : +{ + //String serverName = null, databaseName = null, schemaName = null, tableName = null; + ObjectNames data = null; + Token fileNameToken = null; + Table table; + String timeTravelStr = null; +} +{ + ( + data = RelObjectNames() [ LOOKAHEAD(2) timeTravelStr = TimeTravelBeforeAlias() ] + { + table = createTable(data.getNames()); + table.setTimeTravel(timeTravelStr); + } + | + fileNameToken = + { + // don't split name parts + table = new Table(fileNameToken.image, false); + } + ) + + { + linkAST(table,jjtThis); + return table; + } +} + +/** A table source or DML target; catalog-only DDL continues to use Table(). */ +Table TableReference() #void : +{ + Table table; +} +{ + ( table=TableVariable() | table=Table() ) + { return table; } +} + +Table TableVariable() #TableName : +{ + Token variable; + Table table; +} +{ + variable= + { + requireAccessSyntax(!variable.image.startsWith("@@"), + "A table variable must be a local variable"); + table = new Table(variable.image, false).withTableVariable(true); + linkAST(table, jjtThis); + return table; + } +} + +Table TableWithAlias(): +{ + Table table = null; + Alias alias = null; + SQLServerHints sqlServerHints; +} +{ + table=TableReference() + [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { table.setAlias(alias); }] + [ LOOKAHEAD( "(") sqlServerHints=SQLServerHints() { table.setSqlServerHints(sqlServerHints); } ] + { return table; } +} + +Table TableWithAliasAndMysqlIndexHint(): +{ + Table table = null; + MySQLIndexHint indexHint = null; +} +{ + table=TableWithAlias() + [ LOOKAHEAD(2) indexHint=MySQLIndexHint() { table.setHint(indexHint); } ] + { return table; } +} + +Number Number(): +{ + Token token; + Number number; +} +{ + ( + token = { number = Double.valueOf(token.image); } + | + token = { number = Long.valueOf(token.image); } + ) + + { + return number; + } +} + +SampleClause SampleClause(): +{ + Token token; + SampleClause sampleClause; + String keyword; + String method=null; + Number percentageArgument; + String percentageUnit = null; + boolean argumentInBrackets = true; + Number offsetArgument = null; + Number repeatArgument=null; + Number seedArgument=null; + boolean methodInBrackets = false; +} +{ + ( + ( + // Oracle + token= { keyword = token.image; } + [ token= { method = token.image; } ] + ) + | + ( + // SQL:2016 compliant; DuckDB: the method is optional and may be reservoir + token = { keyword = token.image; } + [ + LOOKAHEAD({ isSampleMethod(getToken(1)) }) + ( token = | token = | token = ) + { method = token.image; } + ] + ) + | + ( + // Duck DB: the method is optional and may follow the sample size instead + { keyword = "USING SAMPLE"; } + [ + LOOKAHEAD({ isSampleMethod(getToken(1)) }) + ( token = | token = | token = ) + { method = token.image; } + ] + ) + ) + + ( + "(" percentageArgument = Number() + [ + "%" { percentageUnit="%"; } + | + { percentageUnit="PERCENT"; } + | + { percentageUnit="ROWS"; } + ] + ")" + + [ LOOKAHEAD(2) "(" repeatArgument = Number() ")" ] + + [ LOOKAHEAD(2) "(" seedArgument = Number() ")" ] + | + percentageArgument = Number() { argumentInBrackets = false; } + // greedy: a unit directly after the sample size always belongs to the clause + [ + LOOKAHEAD(1) + ( + "%" { percentageUnit="%"; } + | + { percentageUnit="PERCENT"; } + | + { percentageUnit="ROWS"; } + ) + ] + // ClickHouse's SAMPLE k OFFSET m; after DuckDB's TABLESAMPLE n, OFFSET belongs to the query + [ LOOKAHEAD(2, { !"TABLESAMPLE".equalsIgnoreCase(keyword) }) offsetArgument = Number() ] + ) + + // DuckDB: USING SAMPLE 10 PERCENT (bernoulli[, seed]) + [ + LOOKAHEAD({ method == null && getToken(1).kind == OPENING_BRACKET + && isSampleMethod(getToken(2)) }) + "(" ( token = | token = | token = ) + { method = token.image; methodInBrackets = true; } + [ "," seedArgument = Number() ] + ")" + ] + + { + sampleClause = new SampleClause(keyword, method, percentageArgument, percentageUnit, repeatArgument, seedArgument); + sampleClause.setArgumentInBrackets(argumentInBrackets); + sampleClause.setMethodInBrackets(methodInBrackets); + sampleClause.setOffsetArgument(offsetArgument); + return sampleClause; + } +} + +Select SelectWithWithItems( List> withItems): +{ + Select select; +} +{ + select = Select() { select.setWithItemsList( withItems ); + return select; +} +} + +Select Select() #Select: +{ + Select select = null; + List> with = null; + List orderByElements = null; + List interpolateElements = null; + OptionClause optionClause = null; + Limit limit = null; + Offset offset = null; + Fetch fetch = null; + WithIsolation withIsolation = null; + Alias alias = null; +} +{ + [ with=WithList() ] + ( + LOOKAHEAD(3, { !isFromFirstSelectAhead() }) select = FromQuery() + | + ( + ( + LOOKAHEAD({ isFromFirstSelectAhead() }) select = FromFirstSelect() + | + // DuckDB's TABLE t, also as a set operation operand + LOOKAHEAD({ getToken(1).kind == K_TABLE }) select = TableStatement() + | + LOOKAHEAD(2) select = PivotQuery() + | + LOOKAHEAD(2) select = UnPivotQuery() + | + LOOKAHEAD(3) select = PlainSelect() + | + LOOKAHEAD(3) select = Values() + | + LOOKAHEAD(3) select = ParenthesedSelect() [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() {select.setAlias(alias);} ] + ) + [ LOOKAHEAD(2) select = FromQueryFromSelect(select) ] + [ LOOKAHEAD(2) select = SetOperationList(select) ] + + [ LOOKAHEAD( ) orderByElements = OrderByElements() { select.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { select.setInterpolate(interpolateElements); } ] + [ LOOKAHEAD(2, { !(select instanceof Values) }) optionClause = OptionClause() { select.setOption(optionClause); } ] + + [ LOOKAHEAD(1, ) limit=LimitWithOffset() {select.setLimit(limit);} ] + [ LOOKAHEAD(1, ) offset = Offset() { select.setOffset(offset);} ] + [ LOOKAHEAD(1, ) fetch = Fetch() { select.setFetch(fetch);} ] + [ LOOKAHEAD( ) withIsolation = WithIsolation() { select.setIsolation(withIsolation);} ] + ) + ) + { + linkAST(select, jjtThis); + return select.withWithItemsList(with); + } +} + +/** + * DuckDB's FROM-first query: FROM tbl [SELECT ...] [WHERE ...] [GROUP BY ...] [HAVING ...] + * [QUALIFY ...] [ORDER BY ...] [LIMIT ...] [OFFSET ...]. Without a SELECT clause all columns + * are returned. + */ +PlainSelect FromFirstSelect() #FromFirstSelect: +{ + PlainSelect plainSelect = new PlainSelect(); + List> selectItems = null; + FromItem fromItem; + List joins = null; + Expression where = null; + GroupByElement groupBy = null; + Expression having = null; + Expression qualify = null; + List orderByElements = null; + Limit limit = null; + Offset offset = null; +} +{ + fromItem=FromItem() + { + plainSelect.setFromItem(fromItem); + plainSelect.setFromFirst(true); + } + [ LOOKAHEAD(2) joins=JoinsList() { plainSelect.setJoins(joins); } ] + // greedy: a SELECT right after the FROM clause belongs to this query + [ + LOOKAHEAD(2) + + [ LOOKAHEAD(2) ( | { plainSelect.setDistinct(new Distinct()); } ) ] + selectItems=SelectItemsList() { plainSelect.setSelectItems(selectItems); } + ] + [ LOOKAHEAD(2) where=WhereClause() { plainSelect.setWhere(where); } ] + [ LOOKAHEAD(2) groupBy=GroupByColumnReferences() { plainSelect.setGroupByElement(groupBy); } ] + [ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); } ] + [ LOOKAHEAD(2) qualify=Qualify() { plainSelect.setQualify(qualify); } ] + [ LOOKAHEAD( ) orderByElements=OrderByElements() + { plainSelect.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD(1, ) limit=LimitWithOffset() { plainSelect.setLimit(limit); } ] + [ LOOKAHEAD(1, ) offset=Offset() { plainSelect.setOffset(offset); } ] + { + linkAST(plainSelect, jjtThis); + return plainSelect; + } +} + +FromQuery FromQuery() #FromQuery: +{ + FromQuery fromQuery; + FromItem fromItem; + List lateralViews = null; + List joins = null; + PipeOperator pipeOperator; +} +{ + fromItem = FromItem() { fromQuery = new FromQuery(fromItem); } + [ LOOKAHEAD(2) lateralViews=LateralViews() { fromQuery.setLateralViews(lateralViews); } ] + [ LOOKAHEAD(2) joins=JoinsList() { fromQuery.setJoins(joins); } ] + ( + LOOKAHEAD(2) "|>" pipeOperator = PipeOperator() { fromQuery.add(pipeOperator); } + )* + + { + return fromQuery; + } +} + +FromQuery FromQueryFromSelect(Select select): +{ + FromQuery fromQuery; + FromItem fromItem; + PipeOperator pipeOperator; +} +{ + "|>" pipeOperator = PipeOperator() + { + fromQuery = new FromQuery(select, false); + fromQuery.add(pipeOperator); + } + ( + LOOKAHEAD(2) "|>" pipeOperator = PipeOperator() + { fromQuery.add(pipeOperator); } + )* + + { + return fromQuery; + } +} + +PipeOperator PipeOperator() #PipeOperator: +{ + PipeOperator operator; +} +{ + ( + // SELECT covers also EXTEND, WINDOW and RENAME + operator = SelectPipeOperator() + | + operator = SetPipeOperator() + | + operator = DropPipeOperator() + | + LOOKAHEAD(2) operator = AsPipeOperator() + | + operator = WherePipeOperator() + | + operator = LimitPipeOperator() + | + LOOKAHEAD(2) operator = AggregatePipeOperator() + | + operator = OrderByPipeOperator() + | + // covers UNION, INTERSET, EXCEPT + operator = SetOperationPipeOperator() + | + operator = JoinPipeOperator() + | + operator = CallPipeOperator() + | + operator = TableSamplePipeOperator() + | + operator = PivotPipeOperator() + | + operator = UnPivotPipeOperator() + ) + { + return operator; + } } -// Sentinel: end of non-reserved keyword range -SKIP: +SelectPipeOperator SelectPipeOperator(): { - + Token operatorKeyToken; + Token modifierToken = null; + SelectPipeOperator selectPipeOperator; + SelectItem selectItem; } +{ + ( + ( operatorKeyToken = [ LOOKAHEAD(2) ( modifierToken= | modifierToken= ) ] ) + | + operatorKeyToken = + | + operatorKeyToken = + | + operatorKeyToken = + ) + selectItem = SelectItem() + { selectPipeOperator = new SelectPipeOperator(operatorKeyToken.image, selectItem, modifierToken !=null ? modifierToken.image : null); } -TOKEN: /* Reserved SQL Keywords and structural tokens */ + ( LOOKAHEAD(2) "," selectItem = SelectItem() { selectPipeOperator.add(selectItem); } )* + + { + return selectPipeOperator; + } +} + +WherePipeOperator WherePipeOperator(): { - -| -| -| -| -| -| -| -| -| /* H2 casewhen function */ -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| /* Salesforce SOQL */ -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| /* Salesforce SOQL */ -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| "> -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| | -| -| -| -| (" ")+ ) > + WherePipeOperator wherePipeOperator; + Expression expression; +} +{ + expression = Expression() + { + wherePipeOperator = new WherePipeOperator(expression); + return wherePipeOperator; + } +} + +String OrderSuffix(): +{ + Token token =null; + String orderSuffix = null; +} +{ + ( token = | token = ) { orderSuffix = token.image; } + [ LOOKAHEAD(2) ( token = | token = ) { orderSuffix += " NULLS " + token.image; } ] + + { + return orderSuffix; + } +} + +AggregatePipeOperator AggregatePipeOperator(): +{ + AggregatePipeOperator aggregatePipeOperator; + SelectItem selectItem; + Token token =null; + String orderSuffix = null; +} +{ + + selectItem = SelectItem() [ LOOKAHEAD(2) orderSuffix = OrderSuffix() ] + { aggregatePipeOperator = new AggregatePipeOperator(selectItem, orderSuffix); orderSuffix=null; } + + ( LOOKAHEAD(2) "," selectItem = SelectItem() [ LOOKAHEAD(2) orderSuffix = OrderSuffix() ] + { aggregatePipeOperator.add(selectItem, orderSuffix); orderSuffix=null; } )* + + [ + LOOKAHEAD(2) [ { aggregatePipeOperator.setShorthandOrdering(true); } ] + selectItem = SelectItem() [ LOOKAHEAD(2) orderSuffix = OrderSuffix() ] + { aggregatePipeOperator.addGroupItem(selectItem, orderSuffix); orderSuffix=null; } + + ( + LOOKAHEAD(2) "," selectItem = SelectItem() [ LOOKAHEAD(2) orderSuffix = OrderSuffix() ] + { aggregatePipeOperator.addGroupItem(selectItem, orderSuffix); orderSuffix=null; } + )* + ] + + { + return aggregatePipeOperator; + } +} + +OrderByPipeOperator OrderByPipeOperator(): +{ + OrderByPipeOperator orderByPipeOperator; + List orderByElements; +} +{ + orderByElements = OrderByElements() + { + orderByPipeOperator = new OrderByPipeOperator(orderByElements); + return orderByPipeOperator; + } +} + +AsPipeOperator AsPipeOperator(): +{ + AsPipeOperator asPipeOperator; + Alias alias; +} +{ + alias = Alias() + { + asPipeOperator = new AsPipeOperator( alias.withUseAs(true) ); + return asPipeOperator; + } +} + +JoinPipeOperator JoinPipeOperator(): +{ + JoinPipeOperator joinPipeOperator; + Join join; +} +{ + join = JoinerExpression() + { + joinPipeOperator = new JoinPipeOperator(join); + return joinPipeOperator; + } +} + +SetPipeOperator SetPipeOperator(): +{ + SetPipeOperator setPipeOperator; + List updateSets; } - -TOKEN : /* Statement Separators */ { - + updateSets = UpdateSets() + { + setPipeOperator = new SetPipeOperator(updateSets); + return setPipeOperator; + } } -TOKEN : /* Operators */ +DropPipeOperator DropPipeOperator(): { - " ()* "="> -| "> -| )* "="> -| )* ">"> -| )* "="> -| -| -| -| -| -| >"> -| -| "> -| + DropPipeOperator dropPipeOperator; + ExpressionList columns; } - -TOKEN : /* Date/Time with time zones */ { - ()* ("(" ")")? ()* ( | ) (()+ )? ()+ "TIME" ()+ > + columns = ColumnList() + { + dropPipeOperator = new DropPipeOperator(columns); + return dropPipeOperator; + } } -TOKEN : /* Data Types */ +LimitPipeOperator LimitPipeOperator(): { - | | | - | | | | | | - | | | | | - | | | | - | | | - ) > - - | <#TYPE_BIT: "BISTRING"> - | <#TYPE_BLOB: "BLOB" | "BYTEA" | | "VARBINARY" | > - | <#TYPE_BOOLEAN: | "BOOL" > - | <#TYPE_CLOB: "CLOB"> - | <#TYPE_ENUM: "ENUM" > - | <#TYPE_MAP: "MAP" > - | <#TYPE_DECIMAL: "DECIMAL" | "NUMBER" | "NUMERIC" > - | <#TYPE_TINYINT: "TINYINT" | "INT1" > - | <#TYPE_SMALLINT: "SMALLINT" | "INT2" | "SHORT" > - | <#TYPE_INTEGER: ( "INTEGER" | "INT" | "INT4" | | ) > - | <#TYPE_BIGINT: "BIGINT" | "INT8" | "LONG" > - | <#TYPE_HUGEINT: "HUGEINT" > - | <#TYPE_UTINYINT: "UTINYINT" > - | <#TYPE_USMALLINT: "USMALLINT" > - | <#TYPE_UINTEGER: "UINTEGER" > - | <#TYPE_UBIGINT: "UBIGINT" > - | <#TYPE_UHUGEINT: "UHUGEINT" > - | <#TYPE_REAL: "REAL" | "FLOAT4" | "FLOAT"> - | <#TYPE_DOUBLE: "DOUBLE" | "PRECISION" | "FLOAT8" | "FLOAT64"> - | <#TYPE_VARCHAR: "NVARCHAR" | "VARCHAR" | "NCHAR" | | "BPCHAR" | "TEXT" | "STRING" | | "VARYING"> - | <#TYPE_TIME: "TIMETZ" > - | <#TYPE_TIMESTAMP: "TIMESTAMP_NS" | "TIMESTAMP_MS" | "TIMESTAMP_S" > + LimitPipeOperator limitPipeOperator; + Expression expression; +} +{ + expression = Expression() { limitPipeOperator = new LimitPipeOperator(expression); } + [ LOOKAHEAD(2) expression = Expression() { limitPipeOperator.setOffsetExpression(expression); } ] - | <#TYPE_UUID: "UUID"> + { + return limitPipeOperator; + } } -TOKEN : /* Numeric Constants */ +// see https://manticore-projects.com/SQL2016Parser/syntax_snapshot.html#corresponding-spec +String SetOperationModifier(): { - < S_DOUBLE: (()? "." ( ["e","E"] (["+", "-"])? )? - | - "." (["e","E"] (["+", "-"])? )? - | - ["e","E"] (["+", "-"])? - )> - | < S_LONG: ( )+ > - | < #DIGIT: ["0" - "9"] > - | < S_HEX: ("X" ("'" ( )* "'" (" ")*)+ | "0x" ( )+ ) > - | < #HEX_VALUE: ["0"-"9","A"-"F", " "] > - | < #HEX_DIGIT: ["0"-"9","A"-"F"] > + Token tk; + String modifier = ""; + String identifier; } - -SPECIAL_TOKEN: { - < LINE_COMMENT: ("--" | "//") (~["\r","\n"])*> + ( + LOOKAHEAD(2) ( + [ ( tk= | tk="DISTINCT") { modifier+=tk.image; } ] + { modifier+= " BY NAME"; } + [ + LOOKAHEAD({ isKeywordAhead("MATCHING") }) + ContextualKeyword("MATCHING") { modifier+= " MATCHING"; } + "(" + identifier = RelObjectName() { modifier+="(" + identifier; } + ("," identifier = RelObjectName() { modifier+=", " + identifier; })* + ")" { modifier+=")"; } + ] + ) + | + ( + [ { modifier+= " STRICT"; } ] + { modifier+= " CORRESPONDING"; } + [ (tk= | tk="DISTINCT") { modifier+=tk.image; } ] + [ + { modifier+= " BY"; }[ (tk= | tk="DISTINCT") { modifier+=tk.image; } ] + "(" + identifier = RelObjectName() { modifier+="(" + identifier; } + ("," identifier = RelObjectName() { modifier+=", " + identifier;})* + ")" { modifier+=")"; } + ] + ) + | + ( tk= | tk= ) { modifier+=tk.image; } + ) + { + return modifier; + } } -// Nested block comments: /* ... /* ... */ ... */ -// -// The body is consumed manually in the MORE action below because JavaCC 8 -// discards actions attached to SPECIAL_TOKEN and SKIP rules: it emits -// SkipLexicalActions() with an empty switch. Only TOKEN and MORE actions -// survive code generation. Do not move this action onto the rule below. -// -// consumeBlockCommentBody() scans past the whole comment, honouring nesting, -// then backs up 2 chars so re-matches the outermost "*/" -// and closes the MORE chain. No character past the comment is ever consumed, -// so there is nothing to push back and no dependency on what follows. -MORE: +SetOperationPipeOperator SetOperationPipeOperator(): { - "/*" { consumeBlockCommentBody(); } : IN_BLOCK_COMMENT + SetOperationPipeOperator setOperationPipeOperator = null; + SetOperationType setOperationType; + String modifier = null; + ParenthesedSelect select; } - - SPECIAL_TOKEN: { - : DEFAULT + ( + ( + [ modifier=SetOperationModifier() ] { setOperationType = SetOperationType.UNION; } + select = ParenthesedSelect() { setOperationPipeOperator = new SetOperationPipeOperator(select, setOperationType, modifier); } + ) + | + ( + [ modifier=SetOperationModifier() ] { setOperationType = SetOperationType.INTERSECT; } + select = ParenthesedSelect() { setOperationPipeOperator = new SetOperationPipeOperator(select, setOperationType, modifier); } + ) + | + ( + [ modifier=SetOperationModifier() ] { setOperationType = SetOperationType.EXCEPT; } + select = ParenthesedSelect() { setOperationPipeOperator = new SetOperationPipeOperator(select, setOperationType, modifier); } + ) + ) + + ( + LOOKAHEAD(2) "," select = ParenthesedSelect() { setOperationPipeOperator.add(select); } + )* + + { + return setOperationPipeOperator; + } } -TOKEN: +CallPipeOperator CallPipeOperator(): { - -| - + TableFunction tableFunction; + Alias alias=null; +} +{ + tableFunction = TableFunction() [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] + { - consumeDollarQuotedString(matchedToken.image); - matchedToken.image = input_stream.GetImage(); - matchedToken.kind = charLiteralIndex; + return new CallPipeOperator(tableFunction, alias); } -| - ()*) | "$" | ("$" ()*)> -| <#LETTER: - | | [ "#", "_" ] // Not SQL:2016 compliant! - > -| <#PART_LETTER_NO_DOLLAR: | | [ "#", "_" , "@" ] > -| <#PART_LETTER: | | [ "$" , "#", "_" , "@" ] > -| ()? > - -// Unicode characters and categories are defined here: https://www.unicode.org/Public/UNIDATA/UnicodeData.txt -// SQL:2016 states: -// An is any character in the Unicode General Category classes “Lu”, “Ll”, “Lt”, “Lm”, “Lo”, or “Nl”. -// An is U+00B7, “Middle Dot”, or any character in the Unicode General Category classes “Mn”, “Mc”, “Nd”, “Pc”, or “Cf”. +} -// unicode_identifier_start -| <#UnicodeIdentifierStart: ("\u00B7" | | | | | | |) > -| <#Ll: ["a"-"z","µ","ß"-"ö","ø"-"ÿ","ā","ă","ą","ć","ĉ","ċ","č","ď","đ","ē","ĕ","ė","ę","ě","ĝ","ğ","ġ","ģ","ĥ","ħ","ĩ","ī","ĭ","į","ı","ij","ĵ","ķ"-"ĸ","ĺ","ļ","ľ","ŀ","ł","ń","ņ","ň"-"ʼn","ŋ","ō","ŏ","ő","œ","ŕ","ŗ","ř","ś","ŝ","ş","š","ţ","ť","ŧ","ũ","ū","ŭ","ů","ű","ų","ŵ","ŷ","ź","ż","ž"-"ƀ","ƃ","ƅ","ƈ","ƌ"-"ƍ","ƒ","ƕ","ƙ"-"ƛ","ƞ","ơ","ƣ","ƥ","ƨ","ƪ"-"ƫ","ƭ","ư","ƴ","ƶ","ƹ"-"ƺ","ƽ"-"ƿ","dž","lj","nj","ǎ","ǐ","ǒ","ǔ","ǖ","ǘ","ǚ","ǜ"-"ǝ","ǟ","ǡ","ǣ","ǥ","ǧ","ǩ","ǫ","ǭ","ǯ"-"ǰ","dz","ǵ","ǹ","ǻ","ǽ","ǿ","ȁ","ȃ","ȅ","ȇ","ȉ","ȋ","ȍ","ȏ","ȑ","ȓ","ȕ","ȗ","ș","ț","ȝ","ȟ","ȡ","ȣ","ȥ","ȧ","ȩ","ȫ","ȭ","ȯ","ȱ","ȳ"-"ȹ","ȼ","ȿ"-"ɀ","ɂ","ɇ","ɉ","ɋ","ɍ","ɏ"-"ʓ","ʕ"-"ʯ","ͱ","ͳ","ͷ","ͻ"-"ͽ","ΐ","ά"-"ώ","ϐ"-"ϑ","ϕ"-"ϗ","ϙ","ϛ","ϝ","ϟ","ϡ","ϣ","ϥ","ϧ","ϩ","ϫ","ϭ","ϯ"-"ϳ","ϵ","ϸ","ϻ"-"ϼ","а"-"џ","ѡ","ѣ","ѥ","ѧ","ѩ","ѫ","ѭ","ѯ","ѱ","ѳ","ѵ","ѷ","ѹ","ѻ","ѽ","ѿ","ҁ","ҋ","ҍ","ҏ","ґ","ғ","ҕ","җ","ҙ","қ","ҝ","ҟ","ҡ","ң","ҥ","ҧ","ҩ","ҫ","ҭ","ү","ұ","ҳ","ҵ","ҷ","ҹ","һ","ҽ","ҿ","ӂ","ӄ","ӆ","ӈ","ӊ","ӌ","ӎ"-"ӏ","ӑ","ӓ","ӕ","ӗ","ә","ӛ","ӝ","ӟ","ӡ","ӣ","ӥ","ӧ","ө","ӫ","ӭ","ӯ","ӱ","ӳ","ӵ","ӷ","ӹ","ӻ","ӽ","ӿ","ԁ","ԃ","ԅ","ԇ","ԉ","ԋ","ԍ","ԏ","ԑ","ԓ","ԕ","ԗ","ԙ","ԛ","ԝ","ԟ","ԡ","ԣ","ԥ","ԧ","ԩ","ԫ","ԭ","ԯ","ՠ"-"ֈ","ა"-"ჺ","ჽ"-"ჿ","ᏸ"-"ᏽ","ᲀ"-"ᲈ","ᴀ"-"ᴫ","ᵫ"-"ᵷ","ᵹ"-"ᶚ","ḁ","ḃ","ḅ","ḇ","ḉ","ḋ","ḍ","ḏ","ḑ","ḓ","ḕ","ḗ","ḙ","ḛ","ḝ","ḟ","ḡ","ḣ","ḥ","ḧ","ḩ","ḫ","ḭ","ḯ","ḱ","ḳ","ḵ","ḷ","ḹ","ḻ","ḽ","ḿ","ṁ","ṃ","ṅ","ṇ","ṉ","ṋ","ṍ","ṏ","ṑ","ṓ","ṕ","ṗ","ṙ","ṛ","ṝ","ṟ","ṡ","ṣ","ṥ","ṧ","ṩ","ṫ","ṭ","ṯ","ṱ","ṳ","ṵ","ṷ","ṹ","ṻ","ṽ","ṿ","ẁ","ẃ","ẅ","ẇ","ẉ","ẋ","ẍ","ẏ","ẑ","ẓ","ẕ"-"ẝ","ẟ","ạ","ả","ấ","ầ","ẩ","ẫ","ậ","ắ","ằ","ẳ","ẵ","ặ","ẹ","ẻ","ẽ","ế","ề","ể","ễ","ệ","ỉ","ị","ọ","ỏ","ố","ồ","ổ","ỗ","ộ","ớ","ờ","ở","ỡ","ợ","ụ","ủ","ứ","ừ","ử","ữ","ự","ỳ","ỵ","ỷ","ỹ","ỻ","ỽ","ỿ"-"ἇ","ἐ"-"ἕ","ἠ"-"ἧ","ἰ"-"ἷ","ὀ"-"ὅ","ὐ"-"ὗ","ὠ"-"ὧ","ὰ"-"ώ","ᾀ"-"ᾇ","ᾐ"-"ᾗ","ᾠ"-"ᾧ","ᾰ"-"ᾴ","ᾶ"-"ᾷ","ι","ῂ"-"ῄ","ῆ"-"ῇ","ῐ"-"ΐ","ῖ"-"ῗ","ῠ"-"ῧ","ῲ"-"ῴ","ῶ"-"ῷ","ℊ","ℎ"-"ℏ","ℓ","ℯ","ℴ","ℹ","ℼ"-"ℽ","ⅆ"-"ⅉ","ⅎ","ↄ","ⰰ"-"ⱟ","ⱡ","ⱥ"-"ⱦ","ⱨ","ⱪ","ⱬ","ⱱ","ⱳ"-"ⱴ","ⱶ"-"ⱻ","ⲁ","ⲃ","ⲅ","ⲇ","ⲉ","ⲋ","ⲍ","ⲏ","ⲑ","ⲓ","ⲕ","ⲗ","ⲙ","ⲛ","ⲝ","ⲟ","ⲡ","ⲣ","ⲥ","ⲧ","ⲩ","ⲫ","ⲭ","ⲯ","ⲱ","ⲳ","ⲵ","ⲷ","ⲹ","ⲻ","ⲽ","ⲿ","ⳁ","ⳃ","ⳅ","ⳇ","ⳉ","ⳋ","ⳍ","ⳏ","ⳑ","ⳓ","ⳕ","ⳗ","ⳙ","ⳛ","ⳝ","ⳟ","ⳡ","ⳣ"-"ⳤ","ⳬ","ⳮ","ⳳ","ⴀ"-"ⴥ","ⴧ","ⴭ","ꙁ","ꙃ","ꙅ","ꙇ","ꙉ","ꙋ","ꙍ","ꙏ","ꙑ","ꙓ","ꙕ","ꙗ","ꙙ","ꙛ","ꙝ","ꙟ","ꙡ","ꙣ","ꙥ","ꙧ","ꙩ","ꙫ","ꙭ","ꚁ","ꚃ","ꚅ","ꚇ","ꚉ","ꚋ","ꚍ","ꚏ","ꚑ","ꚓ","ꚕ","ꚗ","ꚙ","ꚛ","ꜣ","ꜥ","ꜧ","ꜩ","ꜫ","ꜭ","ꜯ"-"ꜱ","ꜳ","ꜵ","ꜷ","ꜹ","ꜻ","ꜽ","ꜿ","ꝁ","ꝃ","ꝅ","ꝇ","ꝉ","ꝋ","ꝍ","ꝏ","ꝑ","ꝓ","ꝕ","ꝗ","ꝙ","ꝛ","ꝝ","ꝟ","ꝡ","ꝣ","ꝥ","ꝧ","ꝩ","ꝫ","ꝭ","ꝯ","ꝱ"-"ꝸ","ꝺ","ꝼ","ꝿ","ꞁ","ꞃ","ꞅ","ꞇ","ꞌ","ꞎ","ꞑ","ꞓ"-"ꞕ","ꞗ","ꞙ","ꞛ","ꞝ","ꞟ","ꞡ","ꞣ","ꞥ","ꞧ","ꞩ","ꞯ","ꞵ","ꞷ","ꞹ","ꞻ","ꞽ","ꞿ","ꟁ","ꟃ","ꟈ","ꟊ","ꟑ","ꟓ","ꟕ","ꟗ","ꟙ","ꟶ","ꟺ","ꬰ"-"ꭚ","ꭠ"-"ꭨ","ꭰ"-"ꮿ","ff"-"st","ﬓ"-"ﬗ","a"-"z"]> -| <#Lm: ["ʰ"-"ˁ","ˆ"-"ˑ","ˠ"-"ˤ","ˬ","ˮ","ʹ","ͺ","ՙ","ـ","ۥ"-"ۦ","ߴ"-"ߵ","ߺ","ࠚ","ࠤ","ࠨ","ࣉ","ॱ","ๆ","ໆ","ჼ","ៗ","ᡃ","ᪧ","ᱸ"-"ᱽ","ᴬ"-"ᵪ","ᵸ","ᶛ"-"ᶿ","ⁱ","ⁿ","ₐ"-"ₜ","ⱼ"-"ⱽ","ⵯ","ⸯ","々","〱"-"〵","〻","ゝ"-"ゞ","ー"-"ヾ","ꀕ","ꓸ"-"ꓽ","ꘌ","ꙿ","ꚜ"-"ꚝ","ꜗ"-"ꜟ","ꝰ","ꞈ","ꟲ"-"ꟴ","ꟸ"-"ꟹ","ꧏ","ꧦ","ꩰ","ꫝ","ꫳ"-"ꫴ","ꭜ"-"ꭟ","ꭩ","ー","゙"-"゚"]> -| <#Lo: ["ª","º","ƻ","ǀ"-"ǃ","ʔ","א"-"ת","ׯ"-"ײ","ؠ"-"ؿ","ف"-"ي","ٮ"-"ٯ","ٱ"-"ۓ","ە","ۮ"-"ۯ","ۺ"-"ۼ","ۿ","ܐ","ܒ"-"ܯ","ݍ"-"ޥ","ޱ","ߊ"-"ߪ","ࠀ"-"ࠕ","ࡀ"-"ࡘ","ࡠ"-"ࡪ","ࡰ"-"ࢇ","ࢉ"-"ࢎ","ࢠ"-"ࣈ","ऄ"-"ह","ऽ","ॐ","क़"-"ॡ","ॲ"-"ঀ","অ"-"ঌ","এ"-"ঐ","ও"-"ন","প"-"র","ল","শ"-"হ","ঽ","ৎ","ড়"-"ঢ়","য়"-"ৡ","ৰ"-"ৱ","ৼ","ਅ"-"ਊ","ਏ"-"ਐ","ਓ"-"ਨ","ਪ"-"ਰ","ਲ"-"ਲ਼","ਵ"-"ਸ਼","ਸ"-"ਹ","ਖ਼"-"ੜ","ਫ਼","ੲ"-"ੴ","અ"-"ઍ","એ"-"ઑ","ઓ"-"ન","પ"-"ર","લ"-"ળ","વ"-"હ","ઽ","ૐ","ૠ"-"ૡ","ૹ","ଅ"-"ଌ","ଏ"-"ଐ","ଓ"-"ନ","ପ"-"ର","ଲ"-"ଳ","ଵ"-"ହ","ଽ","ଡ଼"-"ଢ଼","ୟ"-"ୡ","ୱ","ஃ","அ"-"ஊ","எ"-"ஐ","ஒ"-"க","ங"-"ச","ஜ","ஞ"-"ட","ண"-"த","ந"-"ப","ம"-"ஹ","ௐ","అ"-"ఌ","ఎ"-"ఐ","ఒ"-"న","ప"-"హ","ఽ","ౘ"-"ౚ","ౝ","ౠ"-"ౡ","ಀ","ಅ"-"ಌ","ಎ"-"ಐ","ಒ"-"ನ","ಪ"-"ಳ","ವ"-"ಹ","ಽ","ೝ"-"ೞ","ೠ"-"ೡ","ೱ"-"ೲ","ഄ"-"ഌ","എ"-"ഐ","ഒ"-"ഺ","ഽ","ൎ","ൔ"-"ൖ","ൟ"-"ൡ","ൺ"-"ൿ","අ"-"ඖ","ක"-"න","ඳ"-"ර","ල","ව"-"ෆ","ก"-"ะ","า"-"ำ","เ"-"ๅ","ກ"-"ຂ","ຄ","ຆ"-"ຊ","ຌ"-"ຣ","ລ","ວ"-"ະ","າ"-"ຳ","ຽ","ເ"-"ໄ","ໜ"-"ໟ","ༀ","ཀ"-"ཇ","ཉ"-"ཬ","ྈ"-"ྌ","က"-"ဪ","ဿ","ၐ"-"ၕ","ၚ"-"ၝ","ၡ","ၥ"-"ၦ","ၮ"-"ၰ","ၵ"-"ႁ","ႎ","ᄀ"-"ቈ","ቊ"-"ቍ","ቐ"-"ቖ","ቘ","ቚ"-"ቝ","በ"-"ኈ","ኊ"-"ኍ","ነ"-"ኰ","ኲ"-"ኵ","ኸ"-"ኾ","ዀ","ዂ"-"ዅ","ወ"-"ዖ","ዘ"-"ጐ","ጒ"-"ጕ","ጘ"-"ፚ","ᎀ"-"ᎏ","ᐁ"-"ᙬ","ᙯ"-"ᙿ","ᚁ"-"ᚚ","ᚠ"-"ᛪ","ᛱ"-"ᛸ","ᜀ"-"ᜑ","ᜟ"-"ᜱ","ᝀ"-"ᝑ","ᝠ"-"ᝬ","ᝮ"-"ᝰ","ក"-"ឳ","ៜ","ᠠ"-"ᡂ","ᡄ"-"ᡸ","ᢀ"-"ᢄ","ᢇ"-"ᢨ","ᢪ","ᢰ"-"ᣵ","ᤀ"-"ᤞ","ᥐ"-"ᥭ","ᥰ"-"ᥴ","ᦀ"-"ᦫ","ᦰ"-"ᧉ","ᨀ"-"ᨖ","ᨠ"-"ᩔ","ᬅ"-"ᬳ","ᭅ"-"ᭌ","ᮃ"-"ᮠ","ᮮ"-"ᮯ","ᮺ"-"ᯥ","ᰀ"-"ᰣ","ᱍ"-"ᱏ","ᱚ"-"ᱷ","ᳩ"-"ᳬ","ᳮ"-"ᳳ","ᳵ"-"ᳶ","ᳺ","ℵ"-"ℸ","ⴰ"-"ⵧ","ⶀ"-"ⶖ","ⶠ"-"ⶦ","ⶨ"-"ⶮ","ⶰ"-"ⶶ","ⶸ"-"ⶾ","ⷀ"-"ⷆ","ⷈ"-"ⷎ","ⷐ"-"ⷖ","ⷘ"-"ⷞ","〆","〼","ぁ"-"ゖ","ゟ","ァ"-"ヺ","ヿ","ㄅ"-"ㄯ","ㄱ"-"ㆎ","ㆠ"-"ㆿ","ㇰ"-"ㇿ","䶿","鿿"-"ꀔ","ꀖ"-"ꒌ","ꓐ"-"ꓷ","ꔀ"-"ꘋ","ꘐ"-"ꘟ","ꘪ"-"ꘫ","ꙮ","ꚠ"-"ꛥ","ꞏ","ꟷ","ꟻ"-"ꠁ","ꠃ"-"ꠅ","ꠇ"-"ꠊ","ꠌ"-"ꠢ","ꡀ"-"ꡳ","ꢂ"-"ꢳ","ꣲ"-"ꣷ","ꣻ","ꣽ"-"ꣾ","ꤊ"-"ꤥ","ꤰ"-"ꥆ","ꥠ"-"ꥼ","ꦄ"-"ꦲ","ꧠ"-"ꧤ","ꧧ"-"ꧯ","ꧺ"-"ꧾ","ꨀ"-"ꨨ","ꩀ"-"ꩂ","ꩄ"-"ꩋ","ꩠ"-"ꩯ","ꩱ"-"ꩶ","ꩺ","ꩾ"-"ꪯ","ꪱ","ꪵ"-"ꪶ","ꪹ"-"ꪽ","ꫀ","ꫂ","ꫛ"-"ꫜ","ꫠ"-"ꫪ","ꫲ","ꬁ"-"ꬆ","ꬉ"-"ꬎ","ꬑ"-"ꬖ","ꬠ"-"ꬦ","ꬨ"-"ꬮ","ꯀ"-"ꯢ","힣","ힰ"-"ퟆ","ퟋ"-"ퟻ","豈"-"舘","並"-"龎","יִ","ײַ"-"ﬨ","שׁ"-"זּ","טּ"-"לּ","מּ","נּ"-"סּ","ףּ"-"פּ","צּ"-"ﮱ","ﯓ"-"ﴽ","ﵐ"-"ﶏ","ﶒ"-"ﷇ","ﷰ"-"ﷻ","ﹰ"-"ﹴ","ﹶ"-"ﻼ","ヲ"-"ッ","ア"-"ン","ᅠ"-"ᄒ","ᅡ"-"ᅦ","ᅧ"-"ᅬ","ᅭ"-"ᅲ","ᅳ"-"ᅵ"]> -| <#Lt: ["Dž","Lj","Nj","Dz","ᾈ"-"ᾏ","ᾘ"-"ᾟ","ᾨ"-"ᾯ","ᾼ","ῌ","ῼ"]> -| <#Lu: ["A"-"Z","À"-"Ö","Ø"-"Þ","Ā","Ă","Ą","Ć","Ĉ","Ċ","Č","Ď","Đ","Ē","Ĕ","Ė","Ę","Ě","Ĝ","Ğ","Ġ","Ģ","Ĥ","Ħ","Ĩ","Ī","Ĭ","Į","İ","IJ","Ĵ","Ķ","Ĺ","Ļ","Ľ","Ŀ","Ł","Ń","Ņ","Ň","Ŋ","Ō","Ŏ","Ő","Œ","Ŕ","Ŗ","Ř","Ś","Ŝ","Ş","Š","Ţ","Ť","Ŧ","Ũ","Ū","Ŭ","Ů","Ű","Ų","Ŵ","Ŷ","Ÿ"-"Ź","Ż","Ž","Ɓ"-"Ƃ","Ƅ","Ɔ"-"Ƈ","Ɖ"-"Ƌ","Ǝ"-"Ƒ","Ɠ"-"Ɣ","Ɩ"-"Ƙ","Ɯ"-"Ɲ","Ɵ"-"Ơ","Ƣ","Ƥ","Ʀ"-"Ƨ","Ʃ","Ƭ","Ʈ"-"Ư","Ʊ"-"Ƴ","Ƶ","Ʒ"-"Ƹ","Ƽ","DŽ","LJ","NJ","Ǎ","Ǐ","Ǒ","Ǔ","Ǖ","Ǘ","Ǚ","Ǜ","Ǟ","Ǡ","Ǣ","Ǥ","Ǧ","Ǩ","Ǫ","Ǭ","Ǯ","DZ","Ǵ","Ƕ"-"Ǹ","Ǻ","Ǽ","Ǿ","Ȁ","Ȃ","Ȅ","Ȇ","Ȉ","Ȋ","Ȍ","Ȏ","Ȑ","Ȓ","Ȕ","Ȗ","Ș","Ț","Ȝ","Ȟ","Ƞ","Ȣ","Ȥ","Ȧ","Ȩ","Ȫ","Ȭ","Ȯ","Ȱ","Ȳ","Ⱥ"-"Ȼ","Ƚ"-"Ⱦ","Ɂ","Ƀ"-"Ɇ","Ɉ","Ɋ","Ɍ","Ɏ","Ͱ","Ͳ","Ͷ","Ϳ","Ά","Έ"-"Ί","Ό","Ύ"-"Ώ","Α"-"Ρ","Σ"-"Ϋ","Ϗ","ϒ"-"ϔ","Ϙ","Ϛ","Ϝ","Ϟ","Ϡ","Ϣ","Ϥ","Ϧ","Ϩ","Ϫ","Ϭ","Ϯ","ϴ","Ϸ","Ϲ"-"Ϻ","Ͻ"-"Я","Ѡ","Ѣ","Ѥ","Ѧ","Ѩ","Ѫ","Ѭ","Ѯ","Ѱ","Ѳ","Ѵ","Ѷ","Ѹ","Ѻ","Ѽ","Ѿ","Ҁ","Ҋ","Ҍ","Ҏ","Ґ","Ғ","Ҕ","Җ","Ҙ","Қ","Ҝ","Ҟ","Ҡ","Ң","Ҥ","Ҧ","Ҩ","Ҫ","Ҭ","Ү","Ұ","Ҳ","Ҵ","Ҷ","Ҹ","Һ","Ҽ","Ҿ","Ӏ"-"Ӂ","Ӄ","Ӆ","Ӈ","Ӊ","Ӌ","Ӎ","Ӑ","Ӓ","Ӕ","Ӗ","Ә","Ӛ","Ӝ","Ӟ","Ӡ","Ӣ","Ӥ","Ӧ","Ө","Ӫ","Ӭ","Ӯ","Ӱ","Ӳ","Ӵ","Ӷ","Ӹ","Ӻ","Ӽ","Ӿ","Ԁ","Ԃ","Ԅ","Ԇ","Ԉ","Ԋ","Ԍ","Ԏ","Ԑ","Ԓ","Ԕ","Ԗ","Ԙ","Ԛ","Ԝ","Ԟ","Ԡ","Ԣ","Ԥ","Ԧ","Ԩ","Ԫ","Ԭ","Ԯ","Ա"-"Ֆ","Ⴀ"-"Ⴥ","Ⴧ","Ⴭ","Ꭰ"-"Ᏽ","Ა"-"Ჺ","Ჽ"-"Ჿ","Ḁ","Ḃ","Ḅ","Ḇ","Ḉ","Ḋ","Ḍ","Ḏ","Ḑ","Ḓ","Ḕ","Ḗ","Ḙ","Ḛ","Ḝ","Ḟ","Ḡ","Ḣ","Ḥ","Ḧ","Ḩ","Ḫ","Ḭ","Ḯ","Ḱ","Ḳ","Ḵ","Ḷ","Ḹ","Ḻ","Ḽ","Ḿ","Ṁ","Ṃ","Ṅ","Ṇ","Ṉ","Ṋ","Ṍ","Ṏ","Ṑ","Ṓ","Ṕ","Ṗ","Ṙ","Ṛ","Ṝ","Ṟ","Ṡ","Ṣ","Ṥ","Ṧ","Ṩ","Ṫ","Ṭ","Ṯ","Ṱ","Ṳ","Ṵ","Ṷ","Ṹ","Ṻ","Ṽ","Ṿ","Ẁ","Ẃ","Ẅ","Ẇ","Ẉ","Ẋ","Ẍ","Ẏ","Ẑ","Ẓ","Ẕ","ẞ","Ạ","Ả","Ấ","Ầ","Ẩ","Ẫ","Ậ","Ắ","Ằ","Ẳ","Ẵ","Ặ","Ẹ","Ẻ","Ẽ","Ế","Ề","Ể","Ễ","Ệ","Ỉ","Ị","Ọ","Ỏ","Ố","Ồ","Ổ","Ỗ","Ộ","Ớ","Ờ","Ở","Ỡ","Ợ","Ụ","Ủ","Ứ","Ừ","Ử","Ữ","Ự","Ỳ","Ỵ","Ỷ","Ỹ","Ỻ","Ỽ","Ỿ","Ἀ"-"Ἇ","Ἐ"-"Ἕ","Ἠ"-"Ἧ","Ἰ"-"Ἷ","Ὀ"-"Ὅ","Ὑ","Ὓ","Ὕ","Ὗ","Ὠ"-"Ὧ","Ᾰ"-"Ά","Ὲ"-"Ή","Ῐ"-"Ί","Ῠ"-"Ῥ","Ὸ"-"Ώ","ℂ","ℇ","ℋ"-"ℍ","ℐ"-"ℒ","ℕ","ℙ"-"ℝ","ℤ","Ω","ℨ","K"-"ℭ","ℰ"-"ℳ","ℾ"-"ℿ","ⅅ","Ↄ","Ⰰ"-"Ⱟ","Ⱡ","Ɫ"-"Ɽ","Ⱨ","Ⱪ","Ⱬ","Ɑ"-"Ɒ","Ⱳ","Ⱶ","Ȿ"-"Ⲁ","Ⲃ","Ⲅ","Ⲇ","Ⲉ","Ⲋ","Ⲍ","Ⲏ","Ⲑ","Ⲓ","Ⲕ","Ⲗ","Ⲙ","Ⲛ","Ⲝ","Ⲟ","Ⲡ","Ⲣ","Ⲥ","Ⲧ","Ⲩ","Ⲫ","Ⲭ","Ⲯ","Ⲱ","Ⲳ","Ⲵ","Ⲷ","Ⲹ","Ⲻ","Ⲽ","Ⲿ","Ⳁ","Ⳃ","Ⳅ","Ⳇ","Ⳉ","Ⳋ","Ⳍ","Ⳏ","Ⳑ","Ⳓ","Ⳕ","Ⳗ","Ⳙ","Ⳛ","Ⳝ","Ⳟ","Ⳡ","Ⳣ","Ⳬ","Ⳮ","Ⳳ","Ꙁ","Ꙃ","Ꙅ","Ꙇ","Ꙉ","Ꙋ","Ꙍ","Ꙏ","Ꙑ","Ꙓ","Ꙕ","Ꙗ","Ꙙ","Ꙛ","Ꙝ","Ꙟ","Ꙡ","Ꙣ","Ꙥ","Ꙧ","Ꙩ","Ꙫ","Ꙭ","Ꚁ","Ꚃ","Ꚅ","Ꚇ","Ꚉ","Ꚋ","Ꚍ","Ꚏ","Ꚑ","Ꚓ","Ꚕ","Ꚗ","Ꚙ","Ꚛ","Ꜣ","Ꜥ","Ꜧ","Ꜩ","Ꜫ","Ꜭ","Ꜯ","Ꜳ","Ꜵ","Ꜷ","Ꜹ","Ꜻ","Ꜽ","Ꜿ","Ꝁ","Ꝃ","Ꝅ","Ꝇ","Ꝉ","Ꝋ","Ꝍ","Ꝏ","Ꝑ","Ꝓ","Ꝕ","Ꝗ","Ꝙ","Ꝛ","Ꝝ","Ꝟ","Ꝡ","Ꝣ","Ꝥ","Ꝧ","Ꝩ","Ꝫ","Ꝭ","Ꝯ","Ꝺ","Ꝼ","Ᵹ"-"Ꝿ","Ꞁ","Ꞃ","Ꞅ","Ꞇ","Ꞌ","Ɥ","Ꞑ","Ꞓ","Ꞗ","Ꞙ","Ꞛ","Ꞝ","Ꞟ","Ꞡ","Ꞣ","Ꞥ","Ꞧ","Ꞩ","Ɦ"-"Ɪ","Ʞ"-"Ꞵ","Ꞷ","Ꞹ","Ꞻ","Ꞽ","Ꞿ","Ꟁ","Ꟃ","Ꞔ"-"Ꟈ","Ꟊ","Ꟑ","Ꟗ","Ꟙ","Ꟶ","A"-"Z"]> -| <#Nl: ["ᛮ"-"ᛰ","Ⅰ"-"ↂ","ↅ"-"ↈ","〇","〡"-"〩","〸"-"〺","ꛦ"-"ꛯ"]> -// unicode_identifier_extend -| <#UnicodeIdentifierExtend: (|||||)> -| <#Cf: ["­","؀"-"؅","؜","۝","܏","࢐"-"࢑","࣢","᠎","​"-"‏","‪"-"‮","⁠"-"⁤","⁦"-"","",""-""]> -| <#Mc: ["ः","ऻ","ा"-"ी","ॉ"-"ौ","ॎ"-"ॏ","ং"-"ঃ","া"-"ী","ে"-"ৈ","ো"-"ৌ","ৗ","ਃ","ਾ"-"ੀ","ઃ","ા"-"ી","ૉ","ો"-"ૌ","ଂ"-"ଃ","ା","ୀ","େ"-"ୈ","ୋ"-"ୌ","ୗ","ா"-"ி","ு"-"ூ","ெ"-"ை","ொ"-"ௌ","ௗ","ఁ"-"ః","ు"-"ౄ","ಂ"-"ಃ","ಾ","ೀ"-"ೄ","ೇ"-"ೈ","ೊ"-"ೋ","ೕ"-"ೖ","ೳ","ം"-"ഃ","ാ"-"ീ","െ"-"ൈ","ൊ"-"ൌ","ൗ","ං"-"ඃ","ා"-"ෑ","ෘ"-"ෟ","ෲ"-"ෳ","༾"-"༿","ཿ","ါ"-"ာ","ေ","း","ျ"-"ြ","ၖ"-"ၗ","ၢ"-"ၤ","ၧ"-"ၭ","ႃ"-"ႄ","ႇ"-"ႌ","ႏ","ႚ"-"ႜ","᜕","᜴","ា","ើ"-"ៅ","ះ"-"ៈ","ᤣ"-"ᤦ","ᤩ"-"ᤫ","ᤰ"-"ᤱ","ᤳ"-"ᤸ","ᨙ"-"ᨚ","ᩕ","ᩗ","ᩡ","ᩣ"-"ᩤ","ᩭ"-"ᩲ","ᬄ","ᬵ","ᬻ","ᬽ"-"ᭁ","ᭃ"-"᭄","ᮂ","ᮡ","ᮦ"-"ᮧ","᮪","ᯧ","ᯪ"-"ᯬ","ᯮ","᯲"-"᯳","ᰤ"-"ᰫ","ᰴ"-"ᰵ","᳡","᳷","〮"-"〯","ꠣ"-"ꠤ","ꠧ","ꢀ"-"ꢁ","ꢴ"-"ꣃ","ꥒ"-"꥓","ꦃ","ꦴ"-"ꦵ","ꦺ"-"ꦻ","ꦾ"-"꧀","ꨯ"-"ꨰ","ꨳ"-"ꨴ","ꩍ","ꩻ","ꩽ","ꫫ","ꫮ"-"ꫯ","ꫵ","ꯣ"-"ꯤ","ꯦ"-"ꯧ","ꯩ"-"ꯪ","꯬"]> -| <#Mn: ["̀"-"ͯ","҃"-"҇","֑"-"ֽ","ֿ","ׁ"-"ׂ","ׄ"-"ׅ","ׇ","ؐ"-"ؚ","ً"-"ٟ","ٰ","ۖ"-"ۜ","۟"-"ۤ","ۧ"-"ۨ","۪"-"ۭ","ܑ","ܰ"-"݊","ަ"-"ް","߫"-"߳","߽","ࠖ"-"࠙","ࠛ"-"ࠣ","ࠥ"-"ࠧ","ࠩ"-"࠭","࡙"-"࡛","࢘"-"࢟","࣊"-"࣡","ࣣ"-"ं","ऺ","़","ु"-"ै","्","॑"-"ॗ","ॢ"-"ॣ","ঁ","়","ু"-"ৄ","্","ৢ"-"ৣ","৾","ਁ"-"ਂ","਼","ੁ"-"ੂ","ੇ"-"ੈ","ੋ"-"੍","ੑ","ੰ"-"ੱ","ੵ","ઁ"-"ં","઼","ુ"-"ૅ","ે"-"ૈ","્","ૢ"-"ૣ","ૺ"-"૿","ଁ","଼","ି","ୁ"-"ୄ","୍","୕"-"ୖ","ୢ"-"ୣ","ஂ","ீ","்","ఀ","ఄ","఼","ా"-"ీ","ె"-"ై","ొ"-"్","ౕ"-"ౖ","ౢ"-"ౣ","ಁ","಼","ಿ","ೆ","ೌ"-"್","ೢ"-"ೣ","ഀ"-"ഁ","഻"-"഼","ു"-"ൄ","്","ൢ"-"ൣ","ඁ","්","ි"-"ු","ූ","ั","ิ"-"ฺ","็"-"๎","ັ","ິ"-"ຼ","່"-"໎","༘"-"༙","༵","༷","༹","ཱ"-"ཾ","ྀ"-"྄","྆"-"྇","ྍ"-"ྗ","ྙ"-"ྼ","࿆","ိ"-"ူ","ဲ"-"့","္"-"်","ွ"-"ှ","ၘ"-"ၙ","ၞ"-"ၠ","ၱ"-"ၴ","ႂ","ႅ"-"ႆ","ႍ","ႝ","፝"-"፟","ᜒ"-"᜔","ᜲ"-"ᜳ","ᝒ"-"ᝓ","ᝲ"-"ᝳ","឴"-"឵","ិ"-"ួ","ំ","៉"-"៓","៝","᠋"-"᠍","᠏","ᢅ"-"ᢆ","ᢩ","ᤠ"-"ᤢ","ᤧ"-"ᤨ","ᤲ","᤹"-"᤻","ᨗ"-"ᨘ","ᨛ","ᩖ","ᩘ"-"ᩞ","᩠","ᩢ","ᩥ"-"ᩬ","ᩳ"-"᩼","᩿","᪰"-"᪽","ᪿ"-"ᫎ","ᬀ"-"ᬃ","᬴","ᬶ"-"ᬺ","ᬼ","ᭂ","᭫"-"᭳","ᮀ"-"ᮁ","ᮢ"-"ᮥ","ᮨ"-"ᮩ","᮫"-"ᮭ","᯦","ᯨ"-"ᯩ","ᯭ","ᯯ"-"ᯱ","ᰬ"-"ᰳ","ᰶ"-"᰷","᳐"-"᳒","᳔"-"᳠","᳢"-"᳨","᳭","᳴","᳸"-"᳹","᷀"-"᷿","⃐"-"⃜","⃡","⃥"-"⃰","⳯"-"⳱","⵿","ⷠ"-"ⷿ","〪"-"〭","゙"-"゚","꙯","ꙴ"-"꙽","ꚞ"-"ꚟ","꛰"-"꛱","ꠂ","꠆","ꠋ","ꠥ"-"ꠦ","꠬","꣄"-"ꣅ","꣠"-"꣱","ꣿ","ꤦ"-"꤭","ꥇ"-"ꥑ","ꦀ"-"ꦂ","꦳","ꦶ"-"ꦹ","ꦼ"-"ꦽ","ꧥ","ꨩ"-"ꨮ","ꨱ"-"ꨲ","ꨵ"-"ꨶ","ꩃ","ꩌ","ꩼ","ꪰ","ꪲ"-"ꪴ","ꪷ"-"ꪸ","ꪾ"-"꪿","꫁","ꫬ"-"ꫭ","꫶","ꯥ","ꯨ","꯭","ﬞ","︀"-"️","︠"-"︯"]> -| <#Nd: ["0"-"9","٠"-"٩","۰"-"۹","߀"-"߉","०"-"९","০"-"৯","੦"-"੯","૦"-"૯","୦"-"୯","௦"-"௯","౦"-"౯","೦"-"೯","൦"-"൯","෦"-"෯","๐"-"๙","໐"-"໙","༠"-"༩","၀"-"၉","႐"-"႙","០"-"៩","᠐"-"᠙","᥆"-"᥏","᧐"-"᧙","᪀"-"᪉","᪐"-"᪙","᭐"-"᭙","᮰"-"᮹","᱀"-"᱉","᱐"-"᱙","꘠"-"꘩","꣐"-"꣙","꤀"-"꤉","꧐"-"꧙","꧰"-"꧹","꩐"-"꩙","꯰"-"꯹","0"-"9"]> -| <#Pc: ["‿"-"⁀","⁔","︳"-"︴","﹍"-"﹏","_"]> +TableSamplePipeOperator TableSamplePipeOperator(): +{ + Token token; +} +{ + "(" ( token= | token= ) ")" + { + return new TableSamplePipeOperator( token.image ); + } +} -// CJK Unified Ideographs block according to https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) -| <#CJK: ["\uAC00"-"\uD7A3", "\u4E00"-"\u9FFF"]> +PivotPipeOperator PivotPipeOperator(): +{ + Function aggregateExpression; + Column inputColumn; + List> pivotColumns; + Alias alias = null; +} +{ + "(" aggregateExpression=Function() + inputColumn=Column() + "(" pivotColumns = SelectItemsList() ")" + ")" + [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] + { + return new PivotPipeOperator(aggregateExpression, inputColumn, pivotColumns, alias); + } +} -| < #SPECIAL_ESC: "\\'" > /* Allowing this will break LIKE ... ESCAPE ... */ -| < #ESC: "\\" ["n","t","b","r","f","\\","\""] > -| < S_CHAR_LITERAL: ( - (["U","E","N","R","B"]|"RB"|"_utf8")? - ( - ("'" ( | | ~["'", "\\"] )* "'") | ("'" ("''" | ~["'"])* "'") - // Alternative Oracle Escape Modes - | ("q'{" (~[])* "}'") - | ("q'(" (~[])* ")'") - | ("q'[" (~[])* "]'") - | ("q''" (~[])* "''") - // | ("q'\\" (~[])* "\\'") <--- Does not work - ) - ) > +UnPivotPipeOperator UnPivotPipeOperator(): +{ + Column valuesColumn; + Column nameColumn; + List> pivotColumns; + Alias alias = null; +} +{ + "(" valuesColumn=Column() + nameColumn=Column() + "(" pivotColumns = SelectItemsList() ")" + ")" + [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] { - // contains the token and always the longest match is returned - // So when Backslash is explicitly not allowed as an Escape Character and a is found - // which contains the , then we will need to - // 1) break the at close it with a "'" - // 2) continue tokenizing after that with a new or any other Token - boolean allowEscape = configuration.getAsBoolean(Feature.allowBackslashEscapeCharacter); - String img = matchedToken.image; - int pos; - if (!allowEscape) { - pos = indexOfSequence(img, "\\'"); - if (pos > 0) { - matchedToken.image = "'" + image.substring( 0, image.indexOf("\\'") + 1 ) + "'"; - // `charLiteralIndex` defined in TokenManagerDeclaration above - matchedToken.kind = charLiteralIndex; - input_stream.backup(image.length() + 1 - matchedToken.image.length()); - } - } else { - pos = lastIndexOfSequence(img, "\\''"); - if (pos > 0) { - matchedToken.image = "'" + image.substring( 0, image.lastIndexOf("\\'") + 3); - // `charLiteralIndex` defined in TokenManagerDeclaration above - matchedToken.kind = charLiteralIndex; - input_stream.backup(image.length() + 1 - matchedToken.image.length() ); - } - } + return new UnPivotPipeOperator(valuesColumn, nameColumn, pivotColumns, alias); } -| < S_QUOTED_IDENTIFIER: "\"" ( "\"\"" | ~["\n","\r","\""])* "\"" | ("`" (~["\n","\r","`"])+ "`") | ( "[" (~["\n","\r","]"])* "]" ) > - { - if ( !configuration.getAsBoolean(Feature.allowSquareBracketQuotation) - && matchedToken.image.charAt(0) == '[' ) { +} - matchedToken.image = "["; - // `squaredBracketOpenIndex` defined in TokenManagerDeclaration above - matchedToken.kind = squaredBracketOpenIndex; - input_stream.backup(image.length() - 1); - } - } +TableStatement TableStatement(): +{ + Table table = null; + List orderByElements = null; + Limit limit = null; + Offset offset = null; + TableStatement tableStatement = new TableStatement(); +}{ + + table = Table() + { tableStatement.setTable(table); } + [ LOOKAHEAD( ) orderByElements = OrderByElements() { tableStatement.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD(1, ) limit = LimitWithOffset() { tableStatement.setLimit(limit);} ] + [ LOOKAHEAD(1, ) offset = Offset() { tableStatement.setOffset(offset);} ] + { return tableStatement; } + /* Support operationList */ } -/** - * Parses identifiers including standard SQL identifiers, quoted identifiers, and specific keywords. - * - * This is used in cases where certain SQL keywords (like NAME, NEXT, VALUE, etc.) can appear - * as identifiers (e.g., table names, column names) depending on the SQL dialect or context. - * - * Supported tokens: - * - : Standard unquoted SQL identifier - * - : Quoted identifier (e.g., `identifier` or "identifier") - * - , , , , , , : Specific keywords treated as identifiers - * - * @return Token representing the identifier or keyword used as identifier - */ -Token KeywordOrIdentifier(): +ParenthesedSelect ParenthesedSelect() #ParenthesedSelect: { - Token tk; + ParenthesedSelect parenthesedSelect = new ParenthesedSelect(); + Select select; } { - ( - tk = - | tk = - | tk = - | tk = - | tk = - | tk = - | tk = - | tk = - | tk = - ) - { return tk; } + "(" + select = Select() + ")" + { + linkAST(parenthesedSelect,jjtThis); + return parenthesedSelect.withSelect(select); + } } -Statement Statement() #Statement: +ParenthesedInsert ParenthesedInsert() #ParenthesedInsert: { - IfElseStatement ifElseStatement = null; - Statement stm = null; - Statement stm2 = null; - Expression condition; + ParenthesedInsert parenthesedInsert = new ParenthesedInsert(); + Insert insert; + Token open = getToken(1); + Token statementFirst; } { - ( - try { - ( - condition=Condition() - ( stm = SingleStatement() | stm = Block() ) { ifElseStatement = new IfElseStatement(condition, stm); } - [ LOOKAHEAD(2) - [ { ifElseStatement.setUsingSemicolonForIfStatement(true); } ] - ( stm2 = SingleStatement() | stm2 = Block() ) { ifElseStatement.setElseStatement(stm2); } - ] - - [ - LOOKAHEAD(2) - { if (stm2!=null) - ifElseStatement.setUsingSemicolonForElseStatement(true); - else if (ifElseStatement!=null) - ifElseStatement.setUsingSemicolonForIfStatement(true); - } - ] - ) - | - ( - (stm = SingleStatement() - | stm = Block()) - - ( | ) - ) - | - LOOKAHEAD( { stm==null && getAsBoolean(Feature.allowUnsupportedStatements) } ) stm = UnsupportedStatement() - } catch (ParseException ex) { - if ( getAsBoolean(Feature.allowUnsupportedStatements) ) { - stm = new UnsupportedStatement( stm.toString(), error_skipto(ST_SEMICOLON) ); - } else if ( errorRecovery ) { - parseErrors.add(ex); - error_skipto(ST_SEMICOLON); - stm = null; - } else { - throw ex; - } - } - - ) + "(" { statementFirst = getToken(1); } + insert = Insert() { linkStatement(insert, statementFirst); } + ")" + { + return linked(parenthesedInsert.withInsert(insert), open); + } +} - { - return ifElseStatement!=null ? ifElseStatement : stm; - } +ParenthesedUpdate ParenthesedUpdate() #ParenthesedUpdate: +{ + ParenthesedUpdate parenthesedUpdate = new ParenthesedUpdate(); + Update update; + Token open = getToken(1); + Token statementFirst; +} +{ + "(" { statementFirst = getToken(1); } + update = Update() { linkStatement(update, statementFirst); } + ")" + { + return linked(parenthesedUpdate.withUpdate(update), open); + } } -Statement SingleStatement() : +ParenthesedDelete ParenthesedDelete() #ParenthesedDelete: { - Statement stm = null; - List> with = null; + ParenthesedDelete parenthesedDelete = new ParenthesedDelete(); + Delete delete; + Token open = getToken(1); + Token statementFirst; } { - ( - LOOKAHEAD(3) ( - [ LOOKAHEAD(2) with=WithList() ] - ( - stm = SelectWithWithItems( with ) - | - stm = InsertWithWithItems( with ) - | - stm = UpdateWithWithItems( with ) - | - stm = DeleteWithWithItems( with ) - | - stm = Merge( with ) - ) - ) - | - stm = Select() - | - stm = TableStatement() - | - LOOKAHEAD(3) stm = Upsert() - | - LOOKAHEAD(2) stm = Alter() - | - // @todo: merge this into the ALTER TABLE statement - stm = RenameTableStatement() - | - stm = Create() - | - stm = Drop() - | - stm = Analyze() - | - stm = Truncate() - | - stm = Execute() - | - stm = Set() - | - stm = Reset() - | - stm = Show() - | - stm = RefreshMaterializedView() - | - stm = Use() - | - stm = SavepointStatement() - | - stm = RollbackStatement() - | - stm = Commit() - | - stm = Comment() - | - stm = Describe() - | - stm = Explain() - | - stm = Declare() - | - stm = Grant() - | - stm = PurgeStatement() - | - stm = SessionStatement() - | - stm = LockStatement() - | - LOOKAHEAD({ Dialect.EXASOL.name().equals(getAsString(Feature.dialect)) }) ( stm = Import() | stm = Export() ) - ) - { return stm; } + "(" { statementFirst = getToken(1); } + delete = Delete() { linkStatement(delete, statementFirst); } + ")" + { + return linked(parenthesedDelete.withDelete(delete), open); + } } -Block Block() #Block : { - Statements stmts = new Statements(); - List list = new ArrayList(); - Statement stm; - Block block = new Block(); +LateralView LateralView() #LateralView: +{ + boolean useOuter = false; + Function generatorFunction = null; + String tableName = null; + String columnName = null; + Alias tableAlias = null; + Alias columnAlias = null; } { - - ()* - ( - stm = SingleStatement() - | stm = Block() - ) - - { list.add(stm); } + [ { useOuter=true; } ] + generatorFunction = Function() + [ LOOKAHEAD(2) + tableName=RelObjectName() + { + tableAlias = new Alias(tableName, false); + } + ] + columnName = RelObjectName() { columnAlias = new Alias(columnName, true); } + // Spark SQL supports multiple Alias Columns: https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-lateral-view.html + // we simulate this by setting the alias name to null and then just adding the columns + [ + LOOKAHEAD(2) "," { columnAlias.setName(null); columnAlias.addAliasColumns( columnName); } + columnName = RelObjectName() { columnAlias.addAliasColumns( columnName); } ( - ( - ( - stm = SingleStatement() - | stm = Block() - ) - - { list.add(stm); } - ) - )* - + LOOKAHEAD(2) "," columnName = RelObjectName() { columnAlias.addAliasColumns( columnName); } + )* + ] { - stmts.setStatements(list); - block.setStatements(stmts); + return new LateralView( + useOuter + , generatorFunction + , tableAlias + , columnAlias + ); } +} - [LOOKAHEAD(2) { block.setSemicolonAfterEnd(true); } ] +/** SELECT modifiers are contextual here so other dialects retain identifier spellings. */ +void MySqlSelectModifier(PlainSelect select): +{ + Token token; + MySqlSelectModifier modifier; +} +{ + ( LOOKAHEAD({ getToken(1).kind == K_HIGH_PRIORITY && isMySqlSelectModifierAhead() }) token= + | token= + | token= | token= + | LOOKAHEAD({ getToken(1).kind == K_SQL_NO_CACHE && isMySqlSelectModifierAhead() }) token= + | LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && isMySqlSelectModifierAhead() }) token= ) { - return block; + modifier = MySqlSelectModifier.valueOf(token.image.toUpperCase(Locale.ROOT)); + if ((modifier == MySqlSelectModifier.SQL_CACHE + && select.getMySqlSqlCacheFlag() == MySqlSqlCacheFlags.SQL_NO_CACHE) + || (modifier == MySqlSelectModifier.SQL_NO_CACHE + && select.getMySqlSqlCacheFlag() == MySqlSqlCacheFlags.SQL_CACHE)) { + throw new ParseException("SQL_CACHE and SQL_NO_CACHE cannot be combined"); + } + select.addMySqlSelectModifiers(modifier); } } -Statements Statements() #Statements: { - Statements stmts = new Statements(); - IfElseStatement ifElseStatement = null; - Statement stm = null; - Statement stm2 = null; - Expression condition; +ForClause ForClause() #ForClause: +{ + Token token = null; + ForClause forClause = new ForClause(); } { + ( + token = + | + token = ( - ( )* - - // todo: allow also first statement to be an `UnsupportedStatement` - try { + ( + ( [ LOOKAHEAD(2) "(" ")" ] | ) ( - condition=Condition() - ( stm = SingleStatement() | stm = Block() ) { ifElseStatement = new IfElseStatement(condition, stm); } - [ LOOKAHEAD(2) - [ { ifElseStatement.setUsingSemicolonForIfStatement(true); } ] - ( stm2 = SingleStatement() | stm2 = Block() ) { ifElseStatement.setElseStatement(stm2); } - ] - - { stmts.add( ifElseStatement ); } - - [ - LOOKAHEAD(2) - { if (stm2!=null) - ifElseStatement.setUsingSemicolonForElseStatement(true); - else if (ifElseStatement!=null) - ifElseStatement.setUsingSemicolonForIfStatement(true); - } - ] - ) - | + LOOKAHEAD(2) "," + ( + + | + | [ LOOKAHEAD(2) "(" ")" ] + | + | [ LOOKAHEAD(2) "(" ")" ] + | [ LOOKAHEAD(2) ( | ) ] + ) + )* + ) + | + ( + ( - (stm = SingleStatement() - | stm = Block()) - - ( | ) - ) - { - stmts.add(stm); stm=null; - } - - } catch (ParseException ex) { - if ( getAsBoolean(Feature.allowUnsupportedStatements) ) { - UnsupportedStatement unsupportedStatement = new UnsupportedStatement( stm!=null ? stm.toString() : "", error_skipto(ST_SEMICOLON) ); - if (!unsupportedStatement.isEmpty()) { - stmts.add( unsupportedStatement ); - } - } else if ( errorRecovery ) { - parseErrors.add(ex); - error_skipto(ST_SEMICOLON); - stmts.add( null ); - } else { - throw ex; - } - } - - ) - - ( LOOKAHEAD(2) - ( )* - try { + LOOKAHEAD(2) "," + ( + + | + | [ LOOKAHEAD(2) "(" ")" ] + | + ) + )* + ) + | + ( + [ LOOKAHEAD(2) "(" ")" ] ( - condition=Condition() - ( stm = SingleStatement() | stm = Block() ) { ifElseStatement = new IfElseStatement(condition, stm); } - [ LOOKAHEAD(2) - [ { ifElseStatement.setUsingSemicolonForIfStatement(true); } ] - ( stm2 = SingleStatement() | stm2 = Block() ) { ifElseStatement.setElseStatement(stm2); } - ] - - { stmts.add( ifElseStatement ); } - ) - | + LOOKAHEAD(2) "," + ( + + | + | [ LOOKAHEAD(2) "(" ")" ] + | [ LOOKAHEAD(2) ( | ) ] + ) + )* + ) + ) + | + ( + token = ( | ) + ( + LOOKAHEAD(2) "," ( - stm = SingleStatement() - | stm = Block() - ) { stmts.add(stm); stm=null; } - } catch (ParseException ex) { - if ( getAsBoolean(Feature.allowUnsupportedStatements) ) { - UnsupportedStatement unsupportedStatement = new UnsupportedStatement( stm!=null ? stm.toString() : "" , error_skipto(ST_SEMICOLON) ); - if (!unsupportedStatement.isEmpty()) { - stmts.add( unsupportedStatement ); - } - } else if ( errorRecovery ) { - parseErrors.add(ex); - error_skipto(ST_SEMICOLON); - stmts.add( null ); - } else { - throw ex; - } - } - - ( LOOKAHEAD(2) { if (stm2!=null) - ifElseStatement.setUsingSemicolonForElseStatement(true); - else if (ifElseStatement!=null) - ifElseStatement.setUsingSemicolonForIfStatement(true); - } )* - - )* + [ LOOKAHEAD(2) "(" ")" ] + | + | + ) + )* + ) + ) + { + forClause.setForOption(token.image); + linkAST(forClause,jjtThis); + return forClause; + } +} - [ - LOOKAHEAD( { getAsBoolean(Feature.allowUnsupportedStatements) } ) - stm = UnsupportedStatement() - { - if (!( (UnsupportedStatement) stm).isEmpty()) { - stmts.add( stm ); - } - } - ] - )* +List LateralViews(): +{ + ArrayList lateralViews = new ArrayList(); + LateralView lateralView = null; +} +{ + lateralView = LateralView() { lateralViews.add(lateralView); } + ( LOOKAHEAD(2) lateralView = LateralView() { lateralViews.add(lateralView); } )* - { - return stmts; + return lateralViews; } } -JAVACODE -List error_skipto(int kind) { - ArrayList tokenImages = new ArrayList(); - ParseException e = generateParseException("test"); - Token t; - do { - t = getNextToken(); - if (t.kind != kind && t.kind != EOF) { - tokenImages.add(t.image); +LateralSubSelect LateralSubSelect() #LateralSubSelect: +{ + LateralSubSelect lateralSubSelect = new LateralSubSelect();; + Select select; +} +{ + "(" select = Select() ")" { lateralSubSelect.withSelect(select).setPrefix("LATERAL"); } + { + linkAST(lateralSubSelect,jjtThis); + return lateralSubSelect; } - } while (t.kind != kind && t.kind != EOF); - return tokenImages; } -LockStatement LockStatement(): { - Table table; - boolean noWait = false; - LockMode lockMode; - Token waitSecondsToken = null; - Long waitSeconds = null; -} { - table = Table() - ( - LOOKAHEAD(2) ( { lockMode = LockMode.RowShare; } ) | - ( { lockMode = LockMode.RowExclusive; } ) | - LOOKAHEAD(2) ( { lockMode = LockMode.ShareRowExclusive; } ) | - LOOKAHEAD(2) ( { lockMode = LockMode.ShareUpdate; } ) | - ( { lockMode = LockMode.Share; } ) | - ( { lockMode = LockMode.Exclusive; } ) - ) - - [ { noWait = true; } | waitSecondsToken = { waitSeconds = Long.valueOf(waitSecondsToken.image); } ] - - { - return new LockStatement(table, lockMode, noWait, waitSeconds); - } +PlainSelect PlainSelect() #PlainSelect: +{ + PlainSelect plainSelect = new PlainSelect(); + List> selectItems = null; + FromItem fromItem = null; + List lateralViews = null; + List joins = null; + List> distinctOn = null; + Expression preWhere = null; + Expression where = null; + ForClause forClause = null; + OptionClause optionClause = null; + List orderByElements; + List interpolateElements = null; + GroupByElement groupBy = null; + Expression having = null; + Expression qualify; + Limit limitBy = null; + Limit limit = null; + Offset offset = null; + Fetch fetch = null; + WithIsolation withIsolation = null; + OptimizeFor optimize = null; + Top top = null; + Skip skip = null; + First first = null; + OracleHierarchicalExpression oracleHierarchicalQueryClause = null; + PreferringClause preferringClause = null; + ExpressionList expressionList = null; + Token partitionFirst = null; + List
intoTables = null; + MySqlSelectIntoClause mySqlSelectIntoClause = null; + MySqlProcedureAnalyse mySqlProcedureAnalyse = null; + Table updateTable = null; + List
updateTables = new ArrayList
(); + Wait wait = null; + Token token; + KSQLWindow ksqlWindow = null; + boolean noWait = false; + String windowName = null; + WindowDefinition winDef; + Table intoTempTable = null; + List settings = null; + Distinct distinct; + boolean all = false; } +{ + -LikeClause LikeClause(): { - LikeClause likeClause = new LikeClause(); - Table table; - List> columnsList; -} { - - table = Table() { likeClause.setTable(table); } - [ "(" columnsList = ColumnSelectItemsList() ")" { likeClause.setColumnsList(columnsList); }] - - [ - LOOKAHEAD(2) - ( - { likeClause.setIncludingDefaults(true); } - | { likeClause.setExcludingDefaults(true); } - ) - - ] - - [ - LOOKAHEAD(2) - ( - { likeClause.setIncludingIdentity(true); } - | { likeClause.setExcludingIdentity(true); } - ) - - ] - - [ - LOOKAHEAD(2) - ( - { likeClause.setIncludingComments(true); } - | { likeClause.setExcludingComments(true); } - ) - - ] + [ LOOKAHEAD(2) { plainSelect.setMySqlHintStraightJoin(true); } ] - { - return likeClause; - } -} + { plainSelect.setOracleHint(getOracleHint()); } -Export Export() #Export: { - Export export = new Export(); - Table table; - ParenthesedExpressionList columns; - ParenthesedSelect select; - ExportIntoItem exportIntoItem; -} { - - ( - table = Table() { export.setTable(table); } - [ columns = ParenthesedColumnList() { export.setColumns(columns); } ] - | select = ParenthesedSelect() { export.setSelect(select); } - ) + [ LOOKAHEAD(2) skip = Skip() { plainSelect.setSkip(skip); } ] - - exportIntoItem = ExportIntoItem() { export.setExportIntoItem(exportIntoItem); } + [ LOOKAHEAD(2) first = First() { plainSelect.setFirst(first); } ] - { - return export; - } -} + // Redshift allows TOP before DISTINCT + // https://docs.aws.amazon.com/redshift/latest/dg/r_SELECT_list.html + // @Todo: reflect the order when de-parsing + [ LOOKAHEAD(2) top = Top() { plainSelect.setTop(top); } ] -Import Import() #Import: { - Import impt = new Import(); - Table table; - ParenthesedExpressionList columns; - List importColumns; - ImportFromItem fromItem; -} { - - [ - + ( LOOKAHEAD({ isMySqlSelectModifierAhead() }) MySqlSelectModifier(plainSelect) )* + + [ LOOKAHEAD(2) ( - table = Table() { impt.setTable(table); } - [ columns = ParenthesedColumnList() { impt.setColumns(columns); } ] - | importColumns = ImportColumns() { impt.setImportColumns(importColumns); } + { all = true; } + | + ( + { distinct = new Distinct(); plainSelect.setDistinct(distinct); } + [ LOOKAHEAD(2) "ON" "(" distinctOn=SelectItemsList() { plainSelect.getDistinct().setOnSelectItems(distinctOn); } ")" ] + ) + | + { distinct = new Distinct(); distinct.setUseDistinctRow(true); plainSelect.setDistinct(distinct); } + | + { distinct = new Distinct(true); plainSelect.setDistinct(distinct); } ) ] - - fromItem = ImportFromItem() { impt.setFromItem(fromItem); } - + ( LOOKAHEAD({ isMySqlSelectModifierAhead() }) MySqlSelectModifier(plainSelect) )* { - return impt; + if ((all || plainSelect.getDistinct() != null) + && (getToken(1).kind == K_ALL || getToken(1).kind == K_DISTINCT + || getToken(1).kind == K_DISTINCTROW || getToken(1).kind == K_UNIQUE)) { + throw new ParseException("SELECT cannot combine ALL and DISTINCT qualifiers"); + } } -} -Import SubImport() #SubImport: { - Import impt = new Import(); - List importColumns; - ImportFromItem fromItem; -} { - "(" - - [ importColumns = ImportColumns() { impt.setImportColumns(importColumns); } ] + [ LOOKAHEAD( ( | )) + + ( + { plainSelect.setBigQuerySelectQualifier( PlainSelect.BigQuerySelectQualifier.AS_STRUCT ); } + | + { plainSelect.setBigQuerySelectQualifier( PlainSelect.BigQuerySelectQualifier.AS_VALUE ); } + ) + ] - - fromItem = ImportFromItem() { impt.setFromItem(fromItem); } - ")" + [ LOOKAHEAD(2) top = Top() { plainSelect.setTop(top); } ] - { - return impt; - } -} + ( + // PostgreSQL allows SELECT with no target expressions. + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_FROM }) + { selectItems = new ArrayList>(); } + | + selectItems=SelectItemsList() + ) -List ImportColumns(): { - ImportColumn importColumn; - List importColumns = new ArrayList(); -} { - "(" + [ LOOKAHEAD(1, ) ( - importColumn = ColumnDefinition() - | importColumn = LikeClause() + LOOKAHEAD({ isMySqlSelectIntoAhead() }) + mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.BEFORE_FROM) + { plainSelect.setMySqlSelectIntoClause(mySqlSelectIntoClause); } + | + intoTables = IntoClause() { plainSelect.setIntoTables(intoTables); } ) - { importColumns.add(importColumn); } + ] + [ LOOKAHEAD(2) fromItem=FromItem() + [ LOOKAHEAD(2) lateralViews=LateralViews() ] + [ LOOKAHEAD(2) joins=JoinsList() ] + ] + [ LOOKAHEAD(3) { plainSelect.setUsingOnly(true); } fromItem=FromItem() + [ LOOKAHEAD(2) lateralViews=LateralViews() ] + [ LOOKAHEAD(2) joins=JoinsList() ] + ] - ( - "," + // Clickhouse FINAL as shown at https://clickhouse.com/docs/en/operations/settings/settings#final + [ LOOKAHEAD(2) { plainSelect.setUsingFinal(true); } ] + + [ LOOKAHEAD(2) ksqlWindow=KSQLWindowClause() { plainSelect.setKsqlWindow(ksqlWindow); } ] + [ LOOKAHEAD(2) preWhere=PreWhereClause() { plainSelect.setPreWhere(preWhere); }] + [ LOOKAHEAD(2) where=WhereClause() { plainSelect.setWhere(where); }] + [ LOOKAHEAD(2) oracleHierarchicalQueryClause=OracleHierarchicalQueryClause() { plainSelect.setOracleHierarchical(oracleHierarchicalQueryClause); } ] + [ LOOKAHEAD(2) preferringClause=PreferringClause() { plainSelect.setPreferringClause(preferringClause); } + [LOOKAHEAD(2) partitionFirst= + expressionList=ComplexExpressionList() + { + preferringClause.setPartitionExpressionList(expressionList, false); + linkAST(preferringClause.getPartitionBy(), partitionFirst); + } + ] + ] + // Oracle supports "HAVING" before "GROUP BY", we will simply parse that but won't pay special attention to the order + [ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); }] + [ LOOKAHEAD(2) groupBy=GroupByColumnReferences() { plainSelect.setGroupByElement(groupBy); }] + [ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); }] + [ LOOKAHEAD(2) qualify=Qualify() {plainSelect.setQualify(qualify); }] + [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOracleSiblings(true); plainSelect.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD(2) + windowName = RelObjectName() winDef = windowDefinition() { List winDefs = new ArrayList(); winDefs.add(winDef.withWindowName(windowName)); } + ( LOOKAHEAD(2) "," windowName = RelObjectName() winDef = windowDefinition() { winDefs.add(winDef.withWindowName(windowName)); } )* + { plainSelect.setWindowDefinitions(winDefs); } + // DuckDB: QUALIFY after WINDOW + [ LOOKAHEAD(2, { plainSelect.getQualify() == null }) qualify=Qualify() + { plainSelect.setQualify(qualify); plainSelect.setQualifyAfterWindow(true); } ] + ] + [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ] + [ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ] + [ LOOKAHEAD(2) optionClause = OptionClause() { plainSelect.setOption(optionClause); } ] + [ LOOKAHEAD(2) + ( { plainSelect.setEmitMode(PlainSelect.EmitMode.CHANGES); } + | { plainSelect.setEmitMode(PlainSelect.EmitMode.FINAL); } ) ] + // Parse the LIMIT row count once (this accepts a parenthesized subquery too), then + // optionally attach ClickHouse's `LIMIT ... BY ...`. Checking BY right after the limit + // expression avoids a numeric LOOKAHEAD, which cannot see past a long parenthesized + // subquery and would wrongly commit to LIMIT BY (issue #2359). + [ LOOKAHEAD(1, ) limit = LimitWithOffset() + [ LOOKAHEAD(1, ) expressionList = ExpressionList() { limit.setByExpressions(expressionList); widen(limit, null, getToken(0)); } ] + { + if (limit.getByExpressions() != null) { + plainSelect.setLimitBy(limit); + } else { + plainSelect.setLimit(limit); + } + } ] + [ LOOKAHEAD(1, ) offset = Offset() { plainSelect.setOffset(offset); } ] + [ LOOKAHEAD(, { limit==null }) limit = LimitWithOffset() { plainSelect.setLimit(limit); } ] + [ LOOKAHEAD( ) + mySqlProcedureAnalyse = MySqlProcedureAnalyse() + { plainSelect.setMySqlProcedureAnalyse(mySqlProcedureAnalyse); } + ] + [ LOOKAHEAD(1, ) fetch = Fetch() { plainSelect.setFetch(fetch); } ] + [ LOOKAHEAD( ) withIsolation = WithIsolation() { plainSelect.setIsolation(withIsolation); } ] + [ LOOKAHEAD(2) + ( - importColumn = ColumnDefinition() - | importColumn = LikeClause() + { plainSelect.setForMode(ForMode.UPDATE); } + | { plainSelect.setForMode(ForMode.SHARE); } + | ( { plainSelect.setForMode(ForMode.NO_KEY_UPDATE); }) + | ( { plainSelect.setForMode(ForMode.KEY_SHARE); }) + | ( { plainSelect.setForMode(ForMode.READ_ONLY); }) + | ( { plainSelect.setForMode(ForMode.FETCH_ONLY); }) ) - { importColumns.add(importColumn); } - )* - ")" - + [ LOOKAHEAD(2) + updateTable = Table() { updateTables.add(updateTable); } + ( LOOKAHEAD(2) "," updateTable = Table() { updateTables.add(updateTable); } )* + { plainSelect.setForUpdateTables(updateTables); } + ] + [ LOOKAHEAD(1, ) wait = Wait() { plainSelect.setWait(wait); } ] + [ LOOKAHEAD(2) ( { plainSelect.setNoWait(true); } + | { plainSelect.setSkipLocked(true); }) ] + [ LOOKAHEAD( ) orderByElements = OrderByElements() + { plainSelect.setOrderByElements(orderByElements); plainSelect.setForUpdateBeforeOrderBy(true); } + ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ] + ] + [ LOOKAHEAD({ isMySqlSelectIntoAhead() }) + { + if (mySqlSelectIntoClause != null || intoTables != null) { + throw new ParseException("Only one INTO clause is allowed per SELECT"); + } + } + mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.TRAILING) + { plainSelect.setMySqlSelectIntoClause(mySqlSelectIntoClause); } + ] + [ LOOKAHEAD(2) settings = UpdateSets() { plainSelect.setSettings(settings); } ] + [ LOOKAHEAD(1, ) optimize = OptimizeFor() { plainSelect.setOptimizeFor(optimize); } ] + [ LOOKAHEAD(3) intoTempTable = Table() { plainSelect.setIntoTempTable(intoTempTable);} ] + [ LOOKAHEAD(3) { plainSelect.setUseWithNoLog(true); } ] { - return importColumns; + plainSelect.setSelectItems(selectItems); + plainSelect.setFromItem(fromItem); + if ( lateralViews!=null && lateralViews.size()>0 ) { + plainSelect.setLateralViews( lateralViews ); + } + if ( joins!=null && joins.size()>0 ) { + plainSelect.setJoins( joins ); + } + linkAST(plainSelect,jjtThis); + return plainSelect; } } -ExportIntoItem ExportIntoItem(): { - ExportIntoItem exportIntoItem; - ErrorClause errorClause; -} { - ( - exportIntoItem = DBMSDestination() - | exportIntoItem = FileDestination() - | exportIntoItem = ScriptSourceDestination() - ) - [ LOOKAHEAD(2) errorClause = ErrorClause() { exportIntoItem.setErrorClause(errorClause); } ] - - { - return exportIntoItem; - } +Select SetOperationList(Select select) #SetOperationList: { + SetOperationList list = new SetOperationList(); + List orderByElements = null; + List interpolateElements = null; + Limit limit = null; + Offset offset = null; + Fetch fetch = null; + WithIsolation withIsolation = null; + List(); + List operations = new ArrayList(); + String modifier = null; } - -ImportFromItem ImportFromItem(): { - ImportFromItem importFromItem; - ErrorClause errorClause; -} { - ( - importFromItem = DBMSSource() - | importFromItem = FileSource() - | importFromItem = ScriptSourceDestination() - ) - [ LOOKAHEAD(2) errorClause = ErrorClause() { importFromItem.setErrorClause(errorClause); } ] +{ { - return importFromItem; + selects.add(select); } -} - -DBMSDestination DBMSDestination() #DBMSDestination: { - DBMSDestination dbmsDestination = new DBMSDestination(); - DBMSType dbmsType; - ConnectionDefinition connectionDefinition; - Table table; - ExpressionList columns; - StringValue statement; - List dbmsTableDestinationOptions; -} { - dbmsType = DBMSType() { dbmsDestination.setDestinationType(dbmsType); } - - connectionDefinition = ConnectionDefinition() { dbmsDestination.setConnectionDefinition(connectionDefinition); } - ( - LOOKAHEAD(3) - table = Table() { dbmsDestination.setTable(table); } - [ LOOKAHEAD(2) columns = ParenthesedColumnList() { dbmsDestination.setColumns(columns); } ] - [ LOOKAHEAD(2) dbmsTableDestinationOptions = DBMSTableDestinationOptionList() { dbmsDestination.setDBMSTableDestinationOptions(dbmsTableDestinationOptions); } ] - | statement = ImportExportStatement() { dbmsDestination.setStatement(statement); } - ) + ( LOOKAHEAD(2) { modifier = null; } ( + ( + [ modifier=SetOperationModifier() ] { UnionOp union = new UnionOp(modifier); linkAST(union,jjtThis); operations.add(union); } - { - return dbmsDestination; - } -} + ) + | + ( + [ modifier=SetOperationModifier() ] { IntersectOp intersect = new IntersectOp(modifier); linkAST(intersect,jjtThis); operations.add(intersect); } + ) + | + ( + [ modifier=SetOperationModifier() ] { MinusOp minus = new MinusOp(modifier); linkAST(minus,jjtThis); operations.add(minus); } + ) + | + ( + [ modifier=SetOperationModifier() ] { ExceptOp except = new ExceptOp(modifier); linkAST(except,jjtThis); operations.add(except); } + ) -DBMSTableDestinationOption DBMSTableDestinationOption(): { - DBMSTableDestinationOption dbmsTableDestinationOption; + ) - Token token; - Token token2; - Token token3; -} { - ( ( - token = - | token = - ) { dbmsTableDestinationOption = new DBMSTableDestinationOption(token.image); } - | token = token2 = token3 = { dbmsTableDestinationOption = new DBMSTableDestinationOption(token.image + " " + token2.image, new StringValue(token3.image)); } - ) + LOOKAHEAD({ getToken(1).kind == K_FROM }) select = FromFirstSelect() + | + LOOKAHEAD({ getToken(1).kind == K_TABLE }) select = TableStatement() + | + select = PlainSelect() + | + select = Values() + | + select = ParenthesedSelect() + ) + { + selects.add(select); + } + )+ - { - return dbmsTableDestinationOption; - } -} + [ LOOKAHEAD(2) orderByElements=OrderByElements() {list.setOrderByElements(orderByElements);} ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { list.setInterpolate(interpolateElements); } ] + [ LOOKAHEAD(1, ) limit = LimitWithOffset() { list.setLimit(limit); } ] + [ LOOKAHEAD(1, ) offset = Offset() { list.setOffset(offset); } ] + [ LOOKAHEAD(, { limit==null }) limit = LimitWithOffset() { list.setLimit(limit); } ] + [ LOOKAHEAD(1, ) fetch = Fetch() { list.setFetch(fetch); } ] + [ LOOKAHEAD( ) withIsolation = WithIsolation() { list.setIsolation(withIsolation); } ] -List DBMSTableDestinationOptionList(): { - List dbmsTableDestinationOptions = new ArrayList(); - DBMSTableDestinationOption dbmsTableDestinationOption; -} { - ( LOOKAHEAD(2) dbmsTableDestinationOption = DBMSTableDestinationOption() { dbmsTableDestinationOptions.add(dbmsTableDestinationOption); } )+ { - return dbmsTableDestinationOptions; + if ( selects.get(selects.size()-1) instanceof PlainSelect ) { + PlainSelect ps = (PlainSelect)selects.get(selects.size()-1); + if (ps.getOrderByElements() != null) { + list.setOrderByElements(ps.getOrderByElements()); + ps.setOrderByElements(null); + } + if (ps.getLimit() != null) { + list.setLimit(ps.getLimit()); + ps.setLimit(null); + } + if (ps.getOffset() != null) { + list.setOffset(ps.getOffset()); + ps.setOffset(null); + } + if (ps.getInterpolate() != null) { + list.setInterpolate(ps.getInterpolate()); + ps.setInterpolate(null); + } + if (ps.getOption() != null) { + list.setOption(ps.getOption()); + ps.setOption(null); + } + if (ps.getFetch() != null) { + list.setFetch(ps.getFetch()); + ps.setFetch(null); + } + if (ps.getIsolation() != null) { + list.setIsolation(ps.getIsolation()); + ps.setIsolation(null); + } + } else if (selects.get(selects.size() - 1) instanceof TableStatement) { + TableStatement table = (TableStatement) selects.get(selects.size() - 1); + if (table.getOrderByElements() != null) { + list.setOrderByElements(table.getOrderByElements()); + table.setOrderByElements(null); + } + if (table.getLimit() != null) { + list.setLimit(table.getLimit()); + table.setLimit(null); + } + if (table.getOffset() != null) { + list.setOffset(table.getOffset()); + table.setOffset(null); + } + } + list.setBracketsOpsAndSelects(selects,operations); + return list; } } -DBMSSource DBMSSource() #DBMSSource: { - DBMSSource dbmsSource = new DBMSSource(); - DBMSType dbmsType; - ConnectionDefinition connectionDefinition; - Table table; - ExpressionList columns; - List statements; -} { - dbmsType = DBMSType() { dbmsSource.setSourceType(dbmsType); } - - connectionDefinition = ConnectionDefinition() { dbmsSource.setConnectionDefinition(connectionDefinition); } +List> WithList(): +{ + List> withItemsList = new ArrayList>(); + WithItem with = null; +} +{ + with=WithItem() { withItemsList.add(with); } + ( "," with=WithItem() { withItemsList.add(with); } )* - ( - table = Table() { dbmsSource.setTable(table); } - [ LOOKAHEAD(2) columns = ParenthesedColumnList() { dbmsSource.setColumns(columns); } ] - | statements = ImportExportStatementsList() { dbmsSource.setStatements(statements); } - ) - { - return dbmsSource; - } + { return withItemsList; } } -DBMSType DBMSType(): { - DBMSType dbmsType; - Token tk1; - Token tk2 = null; -} { +WithItem WithItem() #WithItem: +{ + boolean recursive = false; + boolean materialized = false; + boolean usingNot = false; + String name = null; + List> selectItems = null; + WithFunctionDeclaration withFunctionDeclaration = null; + ParenthesedStatement statement = null; + Expression expr = null; + WithSearchClause withSearchClause = null; + WithCycleClause withCycleClause = null; + ExpressionList usingKeyExpressions = null; + WithItem withItem; +} +{ ( - tk1= - | tk1= - | tk1= - [ "=" tk2=] - ) { dbmsType = new DBMSType(tk1.image, tk2 == null ? null : tk2.image); } - { - return dbmsType; - } + LOOKAHEAD(2) + withFunctionDeclaration = WithFunctionDeclaration() + { + withItem = new WithItem().withWithFunctionDeclaration(withFunctionDeclaration); + } + | + LOOKAHEAD({ !isWithExpressionAliasAhead() }) + ( + [ LOOKAHEAD(2) { recursive = true; } ] + name=RelObjectName() + [ "(" selectItems=SelectItemsList() ")" ] + // DuckDB 2.0: WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS (...) + [ LOOKAHEAD(2) "(" usingKeyExpressions=ExpressionList() ")" ] + + [ LOOKAHEAD(2) [ { usingNot = true; } ] { materialized = true; } ] + ( + LOOKAHEAD(2) statement = ParenthesedSelect() + | + LOOKAHEAD(2) statement = ParenthesedInsert() + | + LOOKAHEAD(2) statement = ParenthesedUpdate() + | + LOOKAHEAD(2) statement = ParenthesedDelete() + ) + { + withItem = new WithItem(statement, new Alias(name, false)) + .withRecursive(recursive, usingNot, materialized) + .withWithItemList(selectItems); + withItem.setUsingKeyExpressions(usingKeyExpressions); + } + ) + | + ( + // ClickHouse style expression alias: WITH AS + expr = WithItemExpression() + + name = RelObjectName() + { + withItem = new WithItem().withExpression(expr).withAlias(new Alias(name, false)); + } + ) + ) + [ withSearchClause = WithSearchClause() { withItem.setSearchClause(withSearchClause); } ] + [ withCycleClause = WithCycleClause() { withItem.setCycleClause(withCycleClause); } ] + { + return withItem; + } } -FileType FileType(): { - FileType fileType; - Token tk; -} { +Expression WithItemExpression(): +{ + LambdaExpression lambdaExpression; + Expression expr; + Token open; +} +{ ( - tk= - | tk= - ) { fileType = new FileType(tk.image); } - { - return fileType; - } + // unparenthesized parameter, e.g. x -> x * 2 + LOOKAHEAD( RelObjectName() "->" ) + expr = LambdaExpression() + | + // parenthesized parameter list, e.g. (value) -> value + 1 + LOOKAHEAD( ParenthesedColumnList() "->" ) + expr = LambdaExpression() + | + // whole lambda wrapped in parentheses, e.g. (x -> x * 2) + LOOKAHEAD( "(" ( RelObjectName() | ParenthesedColumnList() ) "->" ) + open="(" lambdaExpression = LambdaExpression() ")" + { expr = new ParenthesedExpressionList(lambdaExpression); linkAST(expr, open); } + | + expr = Expression() + ) + { return expr; } } -StringValue ImportExportStatement() #ImportExportStatement: { - StringValue statement; -} { - token = - { - statement = new StringValue(token.image); - linkAST(statement, jjtThis); - return statement; - } +WithSearchClause WithSearchClause() #WithSearchClause: +{ + Token orderingToken; + ExpressionList searchColumns = new ExpressionList(); + Column searchColumn = null; + String sequenceColumnName; } - -List ImportExportStatementsList(): { - List statements = new ArrayList(); - StringValue statement; -} { - ( statement = ImportExportStatement() { statements.add(statement); } )+ - +{ + + ( orderingToken= | orderingToken= ) + + + searchColumn = Column() { searchColumns.add(searchColumn); } + ( "," searchColumn = Column() { searchColumns.add(searchColumn); } )* + + sequenceColumnName = RelObjectName() { - return statements; + linkElements(searchColumns); + return new WithSearchClause( + "BREADTH".equalsIgnoreCase(orderingToken.image) + ? WithSearchClause.SearchOrder.BREADTH + : WithSearchClause.SearchOrder.DEPTH, + searchColumns, sequenceColumnName); } } -StringValue File() #File: { - Token token; -} { - token = - { - StringValue file = new StringValue(token.image); - linkAST(file, jjtThis); - return file; - } +WithCycleClause WithCycleClause() #WithCycleClause: +{ + WithCycleClause cycle = new WithCycleClause(); + ExpressionList columns = new ExpressionList(); + String name; + Expression value; } - -List FileList(): { - List files = new ArrayList(); - StringValue file; -} { - ( file = File() { files.add(file); } )+ - { - return files; - } +{ + + name=RelObjectName() { columns.add(linkToLastToken(createColumn(name))); } + ( "," name=RelObjectName() { columns.add(linkToLastToken(createColumn(name))); } )* + { linkElements(columns); } + name=RelObjectName() { cycle.setMarkColumnName(name); } + [ value=Expression() { cycle.setMarkValue(value); } + value=Expression() { cycle.setMarkDefault(value); } ] + name=RelObjectName() { cycle.setPathColumnName(name); } + { return cycle.withCycleColumns(columns); } } -ConnectionFileDefinition ConnectionFileDefinition(): { - ConnectionDefinition connectionDefinition = null; - List files; -} { - connectionDefinition = ConnectionOrCloudConnectionDefinition() - files = FileList() - { - return new ConnectionFileDefinition(connectionDefinition, files); - } +WithFunctionDeclaration WithFunctionDeclaration() #WithFunctionDeclaration: +{ + String functionName; + List parameters = new ArrayList(); + String returnType; + Expression returnExpression; + WithFunctionParameter parameter; } - -List ConnectionFileDefinitionList(): { - List connectionFileDefinitions = new ArrayList(); - ConnectionFileDefinition connectionFileDefinition; -} { - ( LOOKAHEAD(2) connectionFileDefinition = ConnectionFileDefinition() { connectionFileDefinitions.add(connectionFileDefinition); } )+ +{ + functionName = RelObjectName() + "(" + [ parameter=WithFunctionParameter() { parameters.add(parameter); } + ( "," parameter=WithFunctionParameter() { parameters.add(parameter); } )* + ] + ")" + returnType = RelObjectName() + returnExpression = Expression() { - return connectionFileDefinitions; + return new WithFunctionDeclaration(functionName, parameters, returnType, returnExpression); } } -CSVColumn CSVDestinationColumn(): { - CSVColumn csvColumn; - - Token token; - Token token2; -} { +WithFunctionParameter WithFunctionParameter() #WithFunctionParameter: +{ + String name; + String type = null; + String arrayType = null; +} +{ + name = RelObjectName() ( - LOOKAHEAD(2) - token= ".." token2= { csvColumn = new CSVColumn(Long.valueOf(token.image), Long.valueOf(token2.image)); } - | token= { csvColumn = new CSVColumn(Long.valueOf(token.image)); } - [ "=" token = { csvColumn.setFormat(new StringValue(token.image)); }] - [ - - "=" - ( - token= - | token= - | token= - ) - { csvColumn.setDelimit(token.image); } - ] + LOOKAHEAD(2) "<" arrayType = RelObjectName() ">" + | + type = RelObjectName() ) { - return csvColumn; + if (arrayType != null) { + type = "ARRAY<" + arrayType + ">"; + } + return new WithFunctionParameter(name, type); } } -List CSVDestinationColumnList(): { - List csvColumns = new ArrayList(); - CSVColumn csvColumn; -} { - csvColumn = CSVDestinationColumn() { csvColumns.add(csvColumn); } - ( "," csvColumn = CSVDestinationColumn() { csvColumns.add(csvColumn); } )* - { - return csvColumns; - } +List> ColumnSelectItemsList(): +{ + List> selectItemsList = null; + SelectItem selectItem = null; } +{ + selectItem=SelectItem() { selectItemsList = new ArrayList>(); selectItemsList.add(selectItem); } + ( + LOOKAHEAD(2) "," selectItem=SelectItem() + { + selectItemsList.add(selectItem); + } + )* -CSVColumn CSVSourceColumn(): { - CSVColumn csvColumn; + { return selectItemsList; } +} - Token token; - Token token2; -} { +List> SelectItemsList(): +{ + List> selectItemsList = null; + SelectItem selectItem = null; +} +{ + selectItem=SelectItem() { selectItemsList = new ArrayList>(); selectItemsList.add(selectItem); } ( - LOOKAHEAD(2) - token= ".." token2= { csvColumn = new CSVColumn(Long.valueOf(token.image), Long.valueOf(token2.image)); } - | token= { csvColumn = new CSVColumn(Long.valueOf(token.image)); } - [ "=" token = { csvColumn.setFormat(new StringValue(token.image)); }] - ) - { - return csvColumn; - } + LOOKAHEAD(2, { !isTrailingSelectItemCommaAhead() }) "," selectItem=SelectItem() + { + selectItemsList.add(selectItem); + } + )* + // DuckDB's trailing comma: SELECT a, b, FROM t + [ LOOKAHEAD({ isTrailingSelectItemCommaAhead() }) "," ] + + { return selectItemsList; } } -List CSVSourceColumnList(): { - List csvColumns = new ArrayList(); - CSVColumn csvColumn; -} { - csvColumn = CSVSourceColumn() { csvColumns.add(csvColumn); } - ( "," csvColumn = CSVSourceColumn() { csvColumns.add(csvColumn); } )* +FunctionAllColumns FunctionAllColumns() #FunctionAllColumns: +{ + Function function; +} +{ + "(" ( "(" )* function=Function() ")" ( ")" )* "." "*" { - return csvColumns; + return new FunctionAllColumns(function); } } -FBVColumn FBVDestinationColumn(): { - FBVColumn fbvColumn; - - Token token; - Token token2; -} { - ( - token= "=" token2= { fbvColumn = new FBVColumn(token.image, new LongValue(token2.image)); } - | ( token= | token= ) "=" token2= { fbvColumn = new FBVColumn(token.image, new StringValue(token2.image)); } - | token= "=" ( token2= | token2= ) { fbvColumn = new FBVColumn(token.image, token2.image); } - ) +SelectItem SelectItem() #SelectItem: +{ + Expression expression; + Alias alias = null; +} +{ + expression = SelectItemExpression() + [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() ] { - return fbvColumn; + SelectItem selectItem = new SelectItem(expression, alias); + linkAST(selectItem,jjtThis); + return selectItem; } } -List FBVDestinationColumnList(): { - List fbvColumns = new ArrayList(); - FBVColumn fbvColumn; - boolean precedesComma; -} { - fbvColumn = FBVDestinationColumn() { fbvColumns.add(fbvColumn); } - ( - { precedesComma = false; } - ["," { precedesComma = true; }] - fbvColumn = FBVDestinationColumn() { fbvColumn.setPrecedesComma(precedesComma); fbvColumns.add(fbvColumn); } - )* - { - return fbvColumns; - } +/** + * Recognizes a transformer without consuming it. APPLY(functionName) intentionally takes + * precedence over its ambiguous alias-column reading. Other aliases use the existing Alias + * grammar, including typed columns, at every transformer boundary. Only the transformer + * prefix is probed: expression bodies are parsed normally by the Pratt parser. + */ +boolean ColumnsTransformerAhead() #void: +{} +{ + LOOKAHEAD( "(" RelObjectName() ")" ) { return true; } + | LOOKAHEAD( ( | ) AliasColumns() ) { return false; } + | LOOKAHEAD( "(" | [ ] "(" + | [ ] ( ParenthesedColumnList() | ) + | ParenthesedColumnList() ) + { return true; } + | { return false; } } -FBVColumn FBVSourceColumn(): { - FBVColumn fbvColumn; +/** + * Recognizes a transformer after the AllColumns-Pattern '*'. + * + * This differs from ColumnsTransformerAhead() in one point: after '*' an alias is not a + * meaningful reading of REPLACE, so REPLACE (col) stays a transformer here whereas + * APPLY (col1, col2) remains an alias. Reusing the COLUMNS(...) rules verbatim would turn + * '* REPLACE (x)' into an alias. + */ +boolean AllColumnsTransformerAhead() #void: +{} +{ + LOOKAHEAD( "(" RelObjectName() ")" ) { return true; } + | LOOKAHEAD( AliasColumns() ) { return false; } + | LOOKAHEAD( "(" | [ ] "(" + | [ ] ( ParenthesedColumnList() | ) + | ParenthesedColumnList() ) + { return true; } + // DuckDB: * EXCLUDE i, * REPLACE expr AS i, * RENAME [(]i AS x[)] + | LOOKAHEAD( Column() ) { return true; } + | LOOKAHEAD({ isUnparenthesizedReplaceAhead() || isRenameAhead() }) { return true; } + | { return false; } +} - Token token; - Token token2; -} { +/** + * Keeps transformer consumption visible to enclosing syntactic lookaheads. The normal + * expression is parsed first; only a direct COLUMNS function can acquire transformers. + * The conditional node encloses both the matcher and its transformers, but adds no AST + * parent for ordinary expressions or alias-column lists. + */ +Expression SelectItemExpression() #ColumnsExpression(transformers != null): +{ + Expression expression; + ColumnsTransformer transformer; + List transformers = null; + Token first; +} +{ + // @fixme: Oracle's SEQUENCE.nextval is parsed as COLUMN with a name part nextval + // @todo: parse a proper SEQUENCE instead of a COLUMN + { first = getToken(1); } ( - ( token= | token= ) "=" token2= { fbvColumn = new FBVColumn(token.image, new LongValue(token2.image)); } - | ( token= | token= ) "=" token2= { fbvColumn = new FBVColumn(token.image, new StringValue(token2.image)); } - | token= "=" ( token2= | token2= ) { fbvColumn = new FBVColumn(token.image, token2.image); } + LOOKAHEAD({ isPriorOperatorAhead() }) expression = ConnectByPriorOperator() { linkASTIfMissing(expression, first); } + | + ( expression = Condition() { expression = prattExpressionRest(expression, first, 2); } ) ) - { - return fbvColumn; - } -} - -List FBVSourceColumnList(): { - List fbvColumns = new ArrayList(); - FBVColumn fbvColumn; - boolean precedesComma; -} { - fbvColumn = FBVSourceColumn() { fbvColumns.add(fbvColumn); } ( - { precedesComma = false; } - ["," { precedesComma = true; }] - fbvColumn = FBVSourceColumn() { fbvColumn.setPrecedesComma(precedesComma); fbvColumns.add(fbvColumn); } + LOOKAHEAD( "(" | [ ] "(" + | [ ] ( ParenthesedColumnList() | ) ) + { + if (!(expression instanceof Function)) { + break; + } + Function function = (Function) expression; + if (!"COLUMNS".equalsIgnoreCase(function.getName()) || function.isEscaped() + || function.getAttribute() != null || function.getChainedParameters() != null + || function.getKeep() != null || function.isIgnoreNullsOutside() + || !ColumnsTransformerAhead()) { + break; + } + } + transformer = ColumnsTransformer() + { + if (transformers == null) { + transformers = new ArrayList(); + } + transformers.add(transformer); + } )* { - return fbvColumns; + if (transformers == null) { + return expression; + } + ColumnsExpression columnsExpression = new ColumnsExpression(expression, transformers); + linkAST(columnsExpression, jjtThis); + return columnsExpression; } } -FileOption FileDestinationOption(): { - FileOption fileOption; - - Token token; - Token token2; - Token token3; -} { +/** + * Parses a single ClickHouse transformer: APPLY(expression), EXCEPT [STRICT] (columns) + * or a pattern, REPLACE [STRICT] (selectItems). ClickHouse parses these in a loop, so + * they may repeat and combine in any order. + */ +ColumnsTransformer ColumnsTransformer(): +{ + Expression applyExpression; + ParenthesedExpressionList exceptColumns = null; + StringValue exceptPattern = null; + List> replaceItems; + SelectItem item; + Column column; + ColumnsTransformer transformer; + Token tk; + boolean strict; + Token first = getToken(1); +} +{ ( - ( token= | token= ) { fileOption = new FileOption(token.image); } - | token= token2= token3= { fileOption = new FileOption(token.image + " " + token2.image + " " + token3.image); } - | ( token= | token= | token= ) "=" token2= { fileOption = new FileOption(token.image, new StringValue(token2.image)); } - | ( - token= token2= "=" token3= - | token= ( token2= | token2= ) "=" token3= - ) - { fileOption = new FileOption(token.image + " " + token2.image, new StringValue(token3.image)); } - | token= "=" ( token2= | token2= | token2= ) { fileOption = new FileOption(token.image, token2.image); } + "(" + ( + LOOKAHEAD( RelObjectName() "->" ) applyExpression = LambdaExpression() + | + applyExpression = Expression() + ) + ")" + { transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.APPLY) + .setApplyExpression(applyExpression); } + + | + { strict = false; } + [ { strict = true; } ] + ( + exceptColumns = ParenthesedColumnList() + | + tk= { exceptPattern = stringValue(tk); } + ) + { transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCEPT) + .setStrict(strict); + if (exceptPattern != null) { + transformer.setExceptPattern(exceptPattern); + } else { + transformer.setExceptColumns(exceptColumns); + } } + + | + { transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE); } + ( + exceptColumns = ParenthesedColumnList() + | + column = Column() + { + exceptColumns = linkElements(new ParenthesedExpressionList(column)); + transformer.setParenthesized(false); + } + ) + { transformer.setExceptColumns(exceptColumns); } + + | + { strict = false; } + [ LOOKAHEAD({ getToken(1).kind == K_STRICT && "(".equals(getToken(2).image) }) + { strict = true; } ] + { transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.REPLACE) + .setStrict(strict); } + ( + LOOKAHEAD({ "(".equals(getToken(1).image) && !isUnparenthesizedReplaceAhead(0) }) + "(" replaceItems = SelectItemsList() ")" + | + item = SelectItem() + { + requireTransformerAlias(item); + replaceItems = new ArrayList>(); + replaceItems.add(item); + transformer.setParenthesized(false); + } + ) + { transformer.setReplaceItems(replaceItems); } + + | + { transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.RENAME); } + ( + LOOKAHEAD(1, "(") "(" replaceItems = SelectItemsList() ")" + | + item = SelectItem() + { + requireTransformerAlias(item); + replaceItems = new ArrayList>(); + replaceItems.add(item); + transformer.setParenthesized(false); + } + ) + { transformer.setRenameItems(replaceItems); } ) - { - return fileOption; - } + { linkAST(transformer, first); return transformer; } } -List FileDestinationOptionList(): { - List fileOptions = new ArrayList(); - FileOption fileOption; -} { - ( LOOKAHEAD(2) fileOption = FileDestinationOption() { fileOptions.add(fileOption); } )+ - { - return fileOptions; - } +/** + * Parses the AllColumns-Pattern '*'. + * + * If the allowAdditions is true, it parses additional Keywords. + */ +AllColumns AllColumns(boolean allowAdditions): +{ + ColumnsTransformer transformer; + List transformers = null; } - -FileOption FileSourceOption(): { - FileOption fileOption; - - Token token; - Token token2; - Token token3; -} { +{ + "*" + // BigData allows EXCEPT, DuckDB allows EXCLUDE and RENAME, ClickHouse allows APPLY; + // they may repeat and combine in any order ( - ( token= | token= | token= ) { fileOption = new FileOption(token.image); } - | ( - ( token= | token= ) "=" token2= { fileOption = new FileOption(token.image, new StringValue(token2.image)); } - | token= "=" token2= { fileOption = new FileOption(token.image, new LongValue(token2.image)); } - ) - | LOOKAHEAD(2) - ( - token= token2= "=" token3= - | token= ( token2= | token2= ) "=" token3= - ) - { fileOption = new FileOption(token.image + " " + token2.image, new StringValue(token3.image)); } - | token= token2= "=" token3= - { fileOption = new FileOption(token.image + " " + token2.image, new LongValue(token3.image)); } - ) + // AllColumnsTransformerAhead() tells DuckDB's REPLACE and RENAME from an alias of the star + LOOKAHEAD( "(" | | + | [ ] ( ParenthesedColumnList() | ) + | ( ParenthesedColumnList() | Column() ) ) + { + if (!allowAdditions || !AllColumnsTransformerAhead()) { + break; + } + } + transformer = ColumnsTransformer() + { + if (transformers == null) { + transformers = new ArrayList(); + } + transformers.add(transformer); + } + )* + { - return fileOption; + return new AllColumns(transformers); } } -List FileSourceOptionList(): { - List fileOptions = new ArrayList(); - FileOption fileOption; -} { - ( LOOKAHEAD(2) fileOption = FileSourceOption() { fileOptions.add(fileOption); } )+ +/** + * Parses the AllTableColumns-Pattern 'table.*' + * + * If the allowAdditions is true, it parses additional Keywords. + */ +AllTableColumns AllTableColumns(boolean allowAdditions): +{ + Table table = null; + AllColumns allColumns; +} +{ + table=Table() "." allColumns=AllColumns(allowAdditions) { - return fileOptions; + return new AllTableColumns(table, allColumns); } + } -FileDestination FileDestination() #FileDestination: { - FileDestination fileDestination = new FileDestination(); - FileType fileType; - List connectionFileDefinitions; - List files; - List csvColumns; - List fbvColumns; - List fileOptions; - CertificateVerification certificateVerification; -} { - ( - fileType = FileType() { fileDestination.setDestinationType(fileType); } - connectionFileDefinitions = ConnectionFileDefinitionList() { fileDestination.setConnectionFileDefinitions(connectionFileDefinitions); } - | { fileDestination.setLocal(true); } - [ { fileDestination.setSecure(true); }] +Alias Alias(): +{ String name = ""; + Token token = null; + boolean useAs = false; + Alias alias; + List aliasColumns; +} +{ + ( + LOOKAHEAD(3) ( + // Aliases with AS and Columns, but optional identifier: + // SELECT fun(x) AS (a,b,c) + // SELECT fun(x) AS T(a,b,c) + + [ LOOKAHEAD(2) name=RelObjectName() ] + { alias = new Alias(name, true ); } - fileType = FileType() { fileDestination.setDestinationType(fileType); } - files = FileList() - { - connectionFileDefinitions = new ArrayList(); - connectionFileDefinitions.add(new ConnectionFileDefinition(files)); - } - ) - { fileDestination.setConnectionFileDefinitions(connectionFileDefinitions); } + aliasColumns = AliasColumns() { alias.setAliasColumns(aliasColumns); } - [ - LOOKAHEAD(2) - "(" - ( - csvColumns = CSVDestinationColumnList() { fileDestination.setCSVColumns(csvColumns); } - | fbvColumns = FBVDestinationColumnList() { fileDestination.setFBVColumns(fbvColumns); } ) - ")" - ] + | + ( + // Aliases with identifier but optional AS and Columns: + // SELECT fun(x) AS T + // SELECT fun(x) T + // SELECT fun(x) T(a,b,c) - [ LOOKAHEAD(2) fileOptions = FileDestinationOptionList() { fileDestination.setFileOptions(fileOptions); } ] + [ { useAs = true; } ] + ( name=RelObjectName() | token= { name=token.image; } ) + { alias = new Alias(name,useAs); } - [ LOOKAHEAD(2) certificateVerification = CertificateVerification() { fileDestination.setCertificateVerification(certificateVerification); } ] + [ LOOKAHEAD(2) aliasColumns = AliasColumns() { alias.setAliasColumns(aliasColumns); } ] - { - return fileDestination; - } + ) + ) + { return alias; } } -FileSource FileSource() #FileSource: { - FileSource fileSource = new FileSource(); - FileType fileType; - List connectionFileDefinitions; - List files; - List csvColumns; - List fbvColumns; - List fileOptions; - CertificateVerification certificateVerification; -} { - ( - fileType = FileType() { fileSource.setSourceType(fileType); } - connectionFileDefinitions = ConnectionFileDefinitionList() { fileSource.setConnectionFileDefinitions(connectionFileDefinitions); } - | { fileSource.setLocal(true); } - [ { fileSource.setSecure(true); }] +List AliasColumns(): +{ + List columns = new ArrayList(); + String name; + ColDataType type = null; +} +{ + "(" name = RelObjectName() [ type = ColDataType() ] + { columns.add(new Alias.AliasColumn(name, type)); } + ( + "," { type = null; } name = RelObjectName() [ type = ColDataType() ] + { columns.add(new Alias.AliasColumn(name, type)); } + )* + ")" + { return columns; } +} - fileType = FileType() { fileSource.setSourceType(fileType); } - files = FileList() - { - connectionFileDefinitions = new ArrayList(); - connectionFileDefinitions.add(new ConnectionFileDefinition(files)); - } - ) - { fileSource.setConnectionFileDefinitions(connectionFileDefinitions); } +void SQLServerHint(SQLServerHints hints) : { + String str; + Token keyword; +} +{ + "(" str = RelObjectName() ")" { hints.setIndexName(str); } + | + { hints.withNoLock(); } + | + ( keyword= | keyword= ) + { + try { + hints.getLockHints().add(SQLServerHints.LockHint.valueOf(keyword.image.toUpperCase(Locale.ROOT))); + } catch (IllegalArgumentException exception) { + throw new ParseException("Unsupported SQL Server locking hint: " + keyword.image); + } + } +} - [ - LOOKAHEAD(2) - "(" - ( - csvColumns = CSVSourceColumnList() { fileSource.setCSVColumns(csvColumns); } - | fbvColumns = FBVSourceColumnList() { fileSource.setFBVColumns(fbvColumns); } - ) +SQLServerHints SQLServerHints() : { + SQLServerHints hints = new SQLServerHints(); +} +{ + "(" + SQLServerHint(hints) ("," SQLServerHint(hints) )* ")" - ] + { return hints; } +} - [ LOOKAHEAD(2) fileOptions = FileSourceOptionList() { fileSource.setFileOptions(fileOptions); } ] +MySQLIndexHint MySQLIndexHint(): +{ + Token actionToken = null; + Token indexToken = null; + String indexName = null; + List indexNameList = new ArrayList(); +} +{ + ( + actionToken = + | actionToken = + | actionToken = + | actionToken = + ) - [ LOOKAHEAD(2) certificateVerification = CertificateVerification() { fileSource.setCertificateVerification(certificateVerification); } ] + ( + indexToken = + | indexToken = + ) - { - return fileSource; - } + "(" + indexName = RelObjectName() { indexNameList.add(indexName); } + ("," indexName= RelObjectName() { indexNameList.add(indexName); })* + ")" + { + return new MySQLIndexHint(actionToken.image, indexToken.image, indexNameList); + } } -CertificateVerification CertificateVerification(): { - CertificateVerification certificateVerification = new CertificateVerification(); - Token token; -} { - ( - ( - { certificateVerification.setIgnoreCertificate(true); } - | { certificateVerification.setVerifyCertificate(true); } - ) - - [ - LOOKAHEAD(2) - - token = { certificateVerification.setPublicKey(new StringValue(token.image)); } - ] - | - token = { certificateVerification.setPublicKey(new StringValue(token.image)); } +SelectItem FunctionItem(): +{ + Alias alias = null; + Function function; + Token first = getToken(1); +} +{ + function=Function() + [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() ] + { + SelectItem item = new SelectItem(function, alias); + linkAST(item, first); + return item; + } +} + +ExpressionList PivotForColumns(): +{ + ExpressionList columns; + Column column; +} +{ + ( + columns = ParenthesedColumnList() + | + column = Column() { columns = linkElements(new ExpressionList(column)); } ) - { - return certificateVerification; - } + { return columns; } } -ScriptSourceDestination ScriptSourceDestination(): { - ScriptSourceDestination scriptSourceDestination = new ScriptSourceDestination(); - ConnectionDefinition connectionDefinition; - Table script; - String property; - StringValue value; - - Token token; -} { - - script = Table() { scriptSourceDestination.setScript(script); } - - [ LOOKAHEAD(2) connectionDefinition = ConnectionDefinition() { scriptSourceDestination.setConnectionDefinition(connectionDefinition); } ] +List> PivotFunctionItems(): +{ + List> functionItems = new ArrayList>(); + SelectItem item; +} +{ + item = FunctionItem() {functionItems.add(item);} + ( LOOKAHEAD(2) "," item = FunctionItem() {functionItems.add(item);} )* + { return functionItems; } +} +/** + * DuckDB's simplified UNPIVOT statement: + * UNPIVOT tbl ON columns [INTO NAME name VALUE values]. + */ +UnPivotQuery UnPivotQuery(): +{ + UnPivotQuery unPivotQuery = new UnPivotQuery(); + ExpressionList onExpressions; + ExpressionList valueColumns; + FromItem fromItem; + String nameColumn; +} +{ + fromItem=FromItem() { unPivotQuery.setFromItem(fromItem); } + onExpressions=ExpressionList() { unPivotQuery.setOnExpressions(onExpressions); } + // greedy: INTO after the ON list belongs to this UNPIVOT [ LOOKAHEAD(2) - { - List properties = new ArrayList(); - List values = new ArrayList(); - scriptSourceDestination.setProperties(properties); - scriptSourceDestination.setValues(values); - } - - - ( - LOOKAHEAD(2) - property = RelObjectName() "=" token = { value = new StringValue(token.image); } - { - properties.add(property); - values.add(value); - } - )+ + nameColumn=RelObjectName() { unPivotQuery.setNameColumn(nameColumn); } + valueColumns=ColumnList() { unPivotQuery.setValueColumns(valueColumns); } ] + { return unPivotQuery; } +} +PivotQuery PivotQuery(): +{ + PivotQuery pivotQuery = new PivotQuery(); + FromItem fromItem; + ExpressionList onExpressions = null; + List> usingItems = null; + ExpressionList groupByExpressions = null; +} +{ + fromItem = FromItem() + ( + onExpressions = ExpressionList() + [ LOOKAHEAD(2) usingItems = PivotFunctionItems() ] + [ LOOKAHEAD(2) groupByExpressions = ExpressionList() ] + | + usingItems = PivotFunctionItems() + [ LOOKAHEAD(2) groupByExpressions = ExpressionList() ] + | + groupByExpressions = ExpressionList() + ) { - return scriptSourceDestination; + pivotQuery.setFromItem(fromItem); + pivotQuery.setOnExpressions(onExpressions); + pivotQuery.setUsingItems(usingItems); + pivotQuery.setGroupByExpressions(groupByExpressions); + return pivotQuery; } } -UserIdentification UserIdentification(): { - UserIdentification userIdentification = new UserIdentification(); - Token token; -} { - - token= { userIdentification.setUser(new StringValue(token.image)); } - - token= { userIdentification.setPassword(new StringValue(token.image)); } - +SelectItem> ExpressionListItem(): +{ + ExpressionList expressionList; + Alias alias = null; + Token first = getToken(1); +} +{ + expressionList=ParenthesedExpressionList() + [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() ] { - return userIdentification; + SelectItem> item = new SelectItem>(expressionList, alias); + linkAST(item, first); + return item; } } -ConnectionDefinition ConnectionDefinition(): { - ConnectionDefinition connectionDefinition = new ConnectionDefinition(); - String connectionObjectName; - UserIdentification userIdentification; - CertificateVerification certificateVerification; - - Token token; -} { - - ( - connectionObjectName = RelObjectName() { connectionDefinition.setConnectionObjectName(connectionObjectName); } - | token= { connectionDefinition.setConnectionDefinition(new StringValue(token.image)); } - ) - - [ LOOKAHEAD(2) userIdentification = UserIdentification() { connectionDefinition.setUserIdentification(userIdentification); } ] - - [ LOOKAHEAD(2) certificateVerification = CertificateVerification() { connectionDefinition.setCertificateVerification(certificateVerification); } ] - - { - return connectionDefinition; - } +List>> PivotMultiInItems(): +{ + List>> retval = new ArrayList>>(); + SelectItem> item; } - -ConnectionDefinition CloudConnectionDefinition(): { - CloudConnectionDefinition connectionDefinition = new CloudConnectionDefinition(); - String connectionObjectName; - UserIdentification userIdentification; - - Token token; - Token token2; -} { - - ( - token = { connectionDefinition.setStorage(token.image); } - | token = token2 = { connectionDefinition.setStorage(token.image + " " + token2.image); } - ) - - ( - connectionObjectName = RelObjectName() { connectionDefinition.setConnectionObjectName(connectionObjectName); } - | token = { connectionDefinition.setConnectionDefinition(new StringValue(token.image)); } - ) - - [ userIdentification = UserIdentification() { connectionDefinition.setUserIdentification(userIdentification); } ] - - { - return connectionDefinition; - } +{ + item = ExpressionListItem() {retval.add(item);} + ("," item = ExpressionListItem() {retval.add(item);} )* + { return retval; } } -ConnectionDefinition ConnectionOrCloudConnectionDefinition(): { - ConnectionDefinition connectionDefinition; -} { - ( - LOOKAHEAD(2) connectionDefinition = CloudConnectionDefinition() - | connectionDefinition = ConnectionDefinition() - ) +Pivot Pivot(): +{ + Pivot retval = new Pivot(); + List> functionItems; + ExpressionList forColumns; + List> singleInItems = null; + List>> multiInItems = null; + Alias alias = null; +} +{ + "(" functionItems = PivotFunctionItems() + forColumns = PivotForColumns() + "(" + (LOOKAHEAD(3) singleInItems = SelectItemsList() + | multiInItems = PivotMultiInItems() ) + ")" + ")" + [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] { - return connectionDefinition; + retval.setFunctionItems(functionItems); + retval.setForColumns(forColumns); + retval.setSingleInItems(singleInItems); + retval.setMultiInItems(multiInItems); + retval.setAlias(alias); + return retval; } } -ErrorClause ErrorClause(): { - ErrorClause errorClause = new ErrorClause(); - ErrorDestination errorDestination; - Expression expression; - RejectClause rejectClause; - - Token token; -} { - ( - - errorDestination = ErrorDestination() { errorClause.setErrorDestination(errorDestination); } - [ - LOOKAHEAD(2) - "(" - expression = Expression() { errorClause.setExpression(expression); } - ")" - ] - [ - LOOKAHEAD(2) - ( - { errorClause.setReplace(true); } - | { errorClause.setTruncate(true); } - ) - ] - [ LOOKAHEAD(2) rejectClause = RejectClause() { errorClause.setRejectClause(rejectClause); } ] - | rejectClause = RejectClause() { errorClause.setRejectClause(rejectClause); } - ) - - { - return errorClause; - } +PivotXml PivotXml(): +{ + PivotXml retval = new PivotXml(); + List> functionItems; + ExpressionList forColumns; + List> singleInItems = null; + List>> multiInItems = null; + Select inSelect = null; } - -RejectClause RejectClause(): { - RejectClause rejectClause = new RejectClause(); -} { - +{ + "(" functionItems = PivotFunctionItems() + forColumns = PivotForColumns() + "(" ( - token= { rejectClause.setLimit(new LongValue(token.image)); } - | + LOOKAHEAD(2) { retval.setInAny(true); } | + LOOKAHEAD(1) inSelect = Select() | + LOOKAHEAD(2) singleInItems =SelectItemsList() | + multiInItems = PivotMultiInItems() ) - - [ LOOKAHEAD(2) { rejectClause.setErrors(true); } ] - + ")" + ")" { - return rejectClause; + retval.setFunctionItems(functionItems); + retval.setForColumns(forColumns); + retval.setSingleInItems(singleInItems); + retval.setMultiInItems(multiInItems); + retval.setInSelect(inSelect); + return retval; } } -ErrorDestination ErrorDestination(): { - ErrorDestination errorDestination; -} { - ( - LOOKAHEAD(2) errorDestination = CSVFileDestination() - | errorDestination = Table() - ) - +UnPivot UnPivot(): +{ + UnPivot retval = new UnPivot(); + ExpressionList unpivotClause; + ExpressionList unpivotForClause; + List> unpivotInClause; + Alias alias = null; +} +{ + + [ ( { retval.setIncludeNulls(true); } + | { retval.setIncludeNulls(false); } ) ] + "(" unpivotClause = PivotForColumns() + unpivotForClause = PivotForColumns() + "(" + unpivotInClause = SelectItemsList() + ")" + ")" + [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] { - return errorDestination; + retval.setUnPivotClause(unpivotClause); + retval.setUnPivotForClause(unpivotForClause); + retval.setUnPivotInClause(unpivotInClause); + retval.setAlias(alias); + return retval; } } -CSVFileDestination CSVFileDestination(): { - CSVFileDestination csvFileDestination = new CSVFileDestination(); - ConnectionDefinition connectionDefinition; - StringValue file; -} { - ( - - connectionDefinition = ConnectionOrCloudConnectionDefinition() { csvFileDestination.setConnectionDefinition(connectionDefinition); } - | { csvFileDestination.setLocal(true); } - [ { csvFileDestination.setSecure(true); } ] - - ) - - file = File() { csvFileDestination.setFile(file); } - +List
IntoClause(): +{ + List
tables = new ArrayList
(); + Table table; +} +{ + table=Table() { tables.add(table); } ( LOOKAHEAD(2) "," table=Table() { tables.add(table); } )* { - return csvFileDestination; + return tables; } } -DeclareStatement Declare(): { - UserVariable userVariable; - ColDataType colDataType; - Expression defaultExpr = null; - DeclareStatement stmt = new DeclareStatement(); - String typeName; - String columnName; - ColumnDefinition colDef; -} { - userVariable = UserVariable() +MySqlSelectIntoClause MySqlSelectIntoClause(MySqlSelectIntoClause.Position position): +{ + MySqlSelectIntoClause intoClause = new MySqlSelectIntoClause().withPosition(position); + Token token; + ExpressionList variables = new ExpressionList(); + UserVariable variable; +} +{ + ( - LOOKAHEAD(2) ( - "(" colDef = ColumnDefinition() - { - stmt.withUserVariable(userVariable) - .withDeclareType(DeclareType.TABLE) - .addColumnDefinition(colDef); - } - ("," colDef = ColumnDefinition() { stmt.addColumnDefinition(colDef); })* ")" - ) - | - typeName = RelObjectName() - { - stmt.withUserVariable(userVariable) - .withDeclareType(DeclareType.AS) - .withTypeName(typeName); - } + { intoClause.setType(MySqlSelectIntoClause.Type.OUTFILE); } + token= { intoClause.setFileName(stringValue(token)); } + MySqlSelectIntoOutfileTail(intoClause) | - (colDataType = ColDataType() ["=" defaultExpr = Expression()] - { - stmt.withDeclareType(DeclareType.TYPE) - .addType(userVariable, colDataType, defaultExpr); - } - ("," userVariable = UserVariable() colDataType = ColDataType() { defaultExpr = null; } - ["=" defaultExpr = Expression()] { stmt.addType(userVariable, colDataType, defaultExpr); } )* - ) + { intoClause.setType(MySqlSelectIntoClause.Type.DUMPFILE); } + token= { intoClause.setFileName(stringValue(token)); } + | + LOOKAHEAD({ isMySqlDialect() }) + variable=MySqlIntoVariable() { variables.add(variable); } + // Same shape as IntoClause() above: a PlainSelect can be followed by "," (subselect in + // an expression list), so the repetition needs two tokens to decide. + ( LOOKAHEAD(2) "," variable=MySqlIntoVariable() { variables.add(variable); } )* + { intoClause.setType(MySqlSelectIntoClause.Type.VARIABLES); intoClause.setVariables(linkElements(variables)); } ) { - return stmt; + return intoClause; } } -SessionStatement SessionStatement(): +UserVariable MySqlIntoVariable(): { - SessionStatement sessionsStatement; - Token actionToken; - Token idToken = null; - String id = null; + UserVariable variable; + Token name; } { - ( | ) ( - actionToken = + variable=UserVariable() | - actionToken = - | - actionToken = - | - actionToken = - | - actionToken = + (name= | name=) + { variable = new UserVariable("@" + name.image); } ) + { return requireMySqlIntoVariable(variable); } +} +MySqlProcedureAnalyse MySqlProcedureAnalyse(): +{ + MySqlProcedureAnalyse procedureAnalyse = new MySqlProcedureAnalyse(); + Token maxElements = null; + Token maxMemory = null; + Token first = getToken(1); +} +{ + "(" [ - ( - idToken = - | - idToken = - | - idToken = - | - idToken = - ) { id = idToken.image; } - - ( - "." - ( - idToken = - | - idToken = - | - idToken = - | - idToken = - ) { id += "." + idToken.image; } - )? - ] - { - sessionsStatement = id!=null - ? new SessionStatement(actionToken.image, id) - : new SessionStatement(actionToken.image); - } - - // options - [ - LOOKAHEAD(2) - ( idToken = | idToken = ) - "=" - ( actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = ) - { sessionsStatement.putOption(idToken.image, actionToken.image ); } - - ( - "," - ( idToken = | idToken = ) - "=" - ( actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = | actionToken = ) - { sessionsStatement.putOption(idToken.image, actionToken.image ); } - )* + maxElements= + [ "," maxMemory= ] ] - + ")" { - //linkAST(sessionsStatement,jjtThis); - return sessionsStatement; + if (maxElements != null) { + procedureAnalyse.setMaxElements(new LongValue(maxElements.image)); + linkAST(procedureAnalyse.getMaxElements(), maxElements, maxElements); + } + if (maxMemory != null) { + procedureAnalyse.setMaxMemory(new LongValue(maxMemory.image)); + linkAST(procedureAnalyse.getMaxMemory(), maxMemory, maxMemory); + } + linkAST(procedureAnalyse, first); + return procedureAnalyse; } } -SetStatement Set(): { - String namePart; - Object name; - ExpressionList expList; - boolean useEqual = false; - SetStatement set; - Expression exp = null; - Token tk = null; - String effectParameter = null; +void MySqlSelectIntoOutfileTail(MySqlSelectIntoClause intoClause): +{ + Token token; } { - - [LOOKAHEAD(3) (tk = | tk = ) {effectParameter = tk.image; } ] ( - LOOKAHEAD(2) - { name = "Time Zone"; useEqual=false; } + LOOKAHEAD( ) + + (token= | token=) + { intoClause.setCharacterSet(token.image); } + ( + LOOKAHEAD(1, ( | )) + MySqlSelectIntoFieldsClause(intoClause) + [ LOOKAHEAD(1, ) MySqlSelectIntoLinesClause(intoClause) ] + | + LOOKAHEAD(1, ) + MySqlSelectIntoLinesClause(intoClause) + | + { } + ) | - ( - name = UserVariable() ["=" { useEqual=true; } ] - ) + LOOKAHEAD(1, ( | )) + MySqlSelectIntoFieldsClause(intoClause) + [ LOOKAHEAD(1, ) MySqlSelectIntoLinesClause(intoClause) ] | - ( - name = IdentifierChain() - ["=" { useEqual=true; } ] - ) + LOOKAHEAD(1, ) + MySqlSelectIntoLinesClause(intoClause) + | + { } ) - exp=Expression() - { - expList = new ExpressionList(); - expList.add(exp); - set = new SetStatement(name, expList) - .withUseEqual(useEqual) - .withEffectParameter(effectParameter); - } - - ( - { useEqual=false; } - "," - (LOOKAHEAD(3) - ( - ( LOOKAHEAD(2) - { name = "Time Zone"; useEqual=false; } - | - (name = RelObjectName() ["=" { useEqual=true; } ]) - ) - exp=Expression() - { - expList = new ExpressionList(); - expList.add(exp); - set.add(name, expList, useEqual); - } - ) - | - exp=Expression() { expList.add(exp); } - ) - )* - { return set; } } -ResetStatement Reset(): { - String name; - ResetStatement reset; - Token all; -} +void MySqlSelectIntoFieldsClause(MySqlSelectIntoClause intoClause): { - ( LOOKAHEAD(2) {name = "Time Zone"; } | name = RelObjectName() | all = {name = all.image; } ) - { reset = new ResetStatement(name); return reset; } -} - -RenameTableStatement RenameTableStatement(): { - RenameTableStatement renameTableStatement; - Table oldName; - Table newName; - boolean usingTableKeyword=false; - boolean usesIfExistsKeyword=false; - String waitDirective = ""; Token token; } { - - [ LOOKAHEAD(2) { usingTableKeyword = true; } ] - [ LOOKAHEAD(2) { usesIfExistsKeyword = true; } ] - oldName = Table() - [ ( - token= { waitDirective = "WAIT " + token.image; } - | - { waitDirective = "NOWAIT"; } - ) ] - - newName = Table() - - { - renameTableStatement = new RenameTableStatement(oldName, newName, usingTableKeyword, usesIfExistsKeyword, waitDirective); - } - ( - "," - oldName = Table() - - newName = Table() - { - renameTableStatement.addTableNames(oldName, newName); - } - )* - - { - return renameTableStatement; - } + { intoClause.setFieldsKeyword(MySqlSelectIntoClause.FieldsKeyword.FIELDS); } + | { intoClause.setFieldsKeyword(MySqlSelectIntoClause.FieldsKeyword.COLUMNS); } + ) + ( + LOOKAHEAD( ) + token= + { intoClause.setFieldsTerminatedBy(stringValue(token)); } + ( + LOOKAHEAD(1, ( | )) + [ { intoClause.setFieldsOptionallyEnclosed(true); } ] + token= + { intoClause.setFieldsEnclosedBy(stringValue(token)); } + [ LOOKAHEAD( ) + token= + { intoClause.setFieldsEscapedBy(stringValue(token)); } + ] + | + LOOKAHEAD( ) + token= + { intoClause.setFieldsEscapedBy(stringValue(token)); } + | + { } + ) + | + LOOKAHEAD(1, ( | )) + [ { intoClause.setFieldsOptionallyEnclosed(true); } ] + token= + { intoClause.setFieldsEnclosedBy(stringValue(token)); } + [ LOOKAHEAD( ) + token= + { intoClause.setFieldsEscapedBy(stringValue(token)); } + ] + | + LOOKAHEAD( ) + token= + { intoClause.setFieldsEscapedBy(stringValue(token)); } + | + { } + ) } -PurgeStatement PurgeStatement(): { - PurgeStatement purgeStatement = null; - Table table; - Index index; - Token tableSpaceToken; - Token userToken = null; +void MySqlSelectIntoLinesClause(MySqlSelectIntoClause intoClause): +{ + Token token; } { - + ( - table=Table() { purgeStatement = new PurgeStatement(table); } - | - index=Index() { purgeStatement = new PurgeStatement(index); } - | - { purgeStatement = new PurgeStatement(PurgeObjectType.RECYCLEBIN); } + LOOKAHEAD( ) + token= + { intoClause.setLinesStartingBy(stringValue(token)); } + [ LOOKAHEAD( ) + token= + { intoClause.setLinesTerminatedBy(stringValue(token)); } + ] | - { purgeStatement = new PurgeStatement(PurgeObjectType.DBA_RECYCLEBIN); } + LOOKAHEAD( ) + token= + { intoClause.setLinesTerminatedBy(stringValue(token)); } | - tableSpaceToken= [ userToken= ] { - purgeStatement = new PurgeStatement( - PurgeObjectType.TABLESPACE - , tableSpaceToken.image - , userToken!=null ? userToken.image : null); - } + { } ) +} - { - return purgeStatement; - } +FromItem ParenthesedFromItem(): +{ + ParenthesedFromItem ParenthesedFromItem = new ParenthesedFromItem(); + FromItem fromItem; + List joins = null; } +{ + "(" + fromItem = FromItem() + [ joins=JoinsList() ] + ")" -DescribeStatement Describe(): { - Table table; - DescribeStatement stmt = new DescribeStatement(); - Token tk = null; -} { - (tk= | tk=) - table = Table() { stmt.setDescribeType(tk.image).setTable(table); } { - return stmt; + return ParenthesedFromItem.withFromItem(fromItem).withJoins(joins); } } -ExplainStatement Explain(): +FromItem FromItem() #FromItem: { - Token tk; - Statement statement; - List> with = null; - Table table; - List options; - ExplainStatement es; + FromItem fromItem = null; + FromItem fromItem2 = null; + SampleClause sampleClause; + Pivot pivot = null; + UnPivot unpivot = null; + Alias alias = null; + MySQLIndexHint indexHint = null; + SQLServerHints sqlServerHints = null; + Select select; + + String timeTravelStr = null; } { - ( tk= | tk = ) ( - LOOKAHEAD(3)( - options= ExplainStatementOptions() - ( - [ LOOKAHEAD(2) with=WithList() ] - ( - statement = SelectWithWithItems( with ) - | statement = InsertWithWithItems( with ) - | statement = UpdateWithWithItems( with ) - | statement = DeleteWithWithItems( with ) - | statement = Merge( with ) - ) - ) - { - es = new ExplainStatement(tk.image, statement, options); - } + LOOKAHEAD(3, { !getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) fromItem = Values() + | + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_TABLE") + && getToken(2).kind == OPENING_BRACKET + }) fromItem=TableFunction() + | + LOOKAHEAD({ + getToken(1).kind == K_LATERAL + && getToken(2).kind == S_IDENTIFIER + && getToken(2).image.equalsIgnoreCase("JSON_TABLE") + && getToken(3).kind == OPENING_BRACKET + }) fromItem=TableFunction() + | + LOOKAHEAD({ + (getToken(1).kind == K_ROWS + && getToken(2).kind == K_FROM + && getToken(3).kind == OPENING_BRACKET) + || (isFunctionAhead() && getToken(1).kind != K_LATERAL) + || (getToken(1).kind == K_LATERAL && getToken(2).kind != OPENING_BRACKET) }) + fromItem=TableFunction() + | + LOOKAHEAD(3) fromItem=TableReference() + | + LOOKAHEAD({ isNestedSetOperationAhead() }) ( + fromItem=ParenthesedSelect() + [ LOOKAHEAD(2) pivot=Pivot() { fromItem.setPivot(pivot); } ] + [ LOOKAHEAD(2) unpivot=UnPivot() { fromItem.setUnPivot(unpivot); } ] ) | - ( - table=Table( ) { es = new ExplainStatement(tk.image, table); } + LOOKAHEAD({ isParenthesedFromItemAhead() }) fromItem = ParenthesedFromItem() + | + LOOKAHEAD(3, { !getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) ( + fromItem=ParenthesedSelect() + [ LOOKAHEAD(2) pivot=Pivot() { fromItem.setPivot(pivot); } ] + [ LOOKAHEAD(2) unpivot=UnPivot() { fromItem.setUnPivot(unpivot); } ] ) + | + fromItem=LateralSubSelect() + | + LOOKAHEAD(2, { Dialect.EXASOL.name().equals(getAsString(Feature.dialect)) }) fromItem=SubImport() { fromItem = new ParenthesedFromItem(fromItem); } + | + LOOKAHEAD({ getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) fromItem=Select() + ) + + [ LOOKAHEAD({ !isMatchRecognizeAhead() && isAliasAhead() }) alias=Alias() { fromItem.setAlias(alias); } ] + [ + // BigQuery: UNNEST(array) AS alias WITH OFFSET AS offsetAlias + LOOKAHEAD(2, { fromItem instanceof TableFunction }) + { ((TableFunction) fromItem).setWithOffset(true); } + [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() + { ((TableFunction) fromItem).setOffsetAlias(alias); } ] + ] + [ + LOOKAHEAD(2, {fromItem instanceof Table }) timeTravelStr = TimeTravelAfterAlias() + { ((Table) fromItem).setTimeTravelStrAfterAlias(timeTravelStr); } + ] + ( + LOOKAHEAD({ isMatchRecognizeAhead() }) fromItem=MatchRecognize(fromItem) + [ LOOKAHEAD(2) ( LOOKAHEAD(2) pivot=PivotXml() | pivot=Pivot() ) { fromItem.setPivot(pivot); } ] + [ LOOKAHEAD(2) unpivot=UnPivot() { fromItem.setUnPivot(unpivot); } ] + [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { fromItem.setAlias(alias); } ] + [ LOOKAHEAD(2) sampleClause=SampleClause() { fromItem.setSampleClause(sampleClause); } ] + | + [ LOOKAHEAD(2) sampleClause = SampleClause() { fromItem.setSampleClause(sampleClause); } ] + [ LOOKAHEAD(2) unpivot=UnPivot() { fromItem.setUnPivot(unpivot); } ] + [ LOOKAHEAD(2) ( LOOKAHEAD(2) pivot=PivotXml() | pivot=Pivot() ) { fromItem.setPivot(pivot); } ] ) + [ + LOOKAHEAD(2) + ( + indexHint = MySQLIndexHint() { + if (fromItem instanceof Table) + ((Table) fromItem).setHint(indexHint); + } + | + sqlServerHints = SQLServerHints() { + if (fromItem instanceof Table) + ((Table) fromItem).setSqlServerHints(sqlServerHints); + } + ) + ] { - return es; + linkAST(fromItem,jjtThis); + return fromItem; } } -/** - * Postgres supports TRUE,ON,1,FALSE,OFF,0 as values - */ -String ExplainOptionBoolean(): -{ - Token tk = null; -} -{ - // intentionally not supporting 0,1 at the moment - [( tk= | tk= | tk= | tk= )] // optional - { - return tk != null ? tk.image : null; - } -} - -/** - * The output format, which can be TEXT, XML, JSON, or YAML - */ -String ExplainFormatOption(): -{ - Token tk = null; -} -{ - // TODO support Text - [( tk= | tk= | tk= )] // optional - { - return tk != null ? tk.image : null; - } -} -/** - * Options for explain, see https://www.postgresql.org/docs/9.1/sql-explain.html - */ -List ExplainStatementOptions(): +MatchRecognize MatchRecognize(FromItem input) #MatchRecognize: { - List options = new ArrayList(); - ExplainStatement.Option option = null; - Token token = null; - String value = null; + MatchRecognize result = new MatchRecognize(input); + ExpressionList partition; + List ordering; + List> measures; + MatchRecognize.Definition definition; + MatchRecognize.Subset subset; + RowPattern pattern; + MatchRecognize.Options options = null; + String variable; + Token value; } { - ( - ( value=ExplainOptionBoolean() - { - option = new ExplainStatement.Option(ExplainStatement.OptionType.ANALYZE); - option.setValue(value); - options.add(option); - } - ) - | - ( value=ExplainOptionBoolean() - { - option = new ExplainStatement.Option(ExplainStatement.OptionType.BUFFERS); - option.setValue(value); - options.add(option); - } - ) - | - ( value=ExplainOptionBoolean() - { - option = new ExplainStatement.Option(ExplainStatement.OptionType.COSTS); - option.setValue(value); - options.add(option); - } - ) - | - ( value=ExplainOptionBoolean() - { - option = new ExplainStatement.Option(ExplainStatement.OptionType.VERBOSE); - option.setValue(value); - options.add(option); - } - ) - | - ( value=ExplainFormatOption() - { - option = new ExplainStatement.Option(ExplainStatement.OptionType.FORMAT); - option.setValue(value); - options.add(option); - } - ) - | - ( - { option = new ExplainStatement.Option(ExplainStatement.OptionType.PLAN); } - [ { option = new ExplainStatement.Option(ExplainStatement.OptionType.PLAN_FOR); } ] - - value=ExplainFormatOption() + ContextualKeyword("MATCH_RECOGNIZE") "(" + [ partition=SimpleExpressionList() { result.setPartitionBy(partition); } ] + [ LOOKAHEAD(2) ordering=OrderByElements() { result.setOrderByElements(ordering); } ] + [ LOOKAHEAD({ isKeywordAhead("MEASURES") }) ContextualKeyword("MEASURES") measures=SelectItemsList() { - option.setValue(value); - options.add(option); + for (SelectItem measure : measures) { + if (measure.getAlias() == null) { throw new ParseException("MATCH_RECOGNIZE measures require an alias"); } + } + result.setMeasures(measures); + } + ] + [ + LOOKAHEAD({ isKeywordAhead("ONE") || getToken(1).kind == K_ALL }) + ( LOOKAHEAD({ isKeywordAhead("ONE") }) ContextualKeyword("ONE") ContextualKeyword("PER") + { result.setRowsPerMatch(MatchRecognize.RowsPerMatch.ONE); } + | ContextualKeyword("PER") + { result.setRowsPerMatch(MatchRecognize.RowsPerMatch.ALL); } + [ + ContextualKeyword("MATCHES") + { result.setEmptyMatchMode(MatchRecognize.EmptyMatchMode.SHOW); } + | LOOKAHEAD({ isKeywordAhead("OMIT") }) ContextualKeyword("OMIT") ContextualKeyword("MATCHES") + { result.setEmptyMatchMode(MatchRecognize.EmptyMatchMode.OMIT); } + | ContextualKeyword("UNMATCHED") + { result.setEmptyMatchMode(MatchRecognize.EmptyMatchMode.WITH_UNMATCHED); } + ] + ) + ] + [ LOOKAHEAD({ isKeywordAhead("AFTER") }) ContextualKeyword("AFTER") + ( LOOKAHEAD({ isKeywordAhead("PAST") }) ContextualKeyword("PAST") + { result.setSkipMode(MatchRecognize.SkipMode.PAST_LAST_ROW); } + | + ( LOOKAHEAD(2) { result.setSkipMode(MatchRecognize.SkipMode.TO_NEXT_ROW); } + | { result.setSkipMode(MatchRecognize.SkipMode.TO_VARIABLE); } + [ LOOKAHEAD({ (getToken(1).kind == K_FIRST || getToken(1).kind == K_LAST) + && !("PATTERN".equalsIgnoreCase(getToken(2).image) && getToken(3).kind == OPENING_BRACKET) }) + ( { result.setSkipMode(MatchRecognize.SkipMode.TO_FIRST); } + | { result.setSkipMode(MatchRecognize.SkipMode.TO_LAST); } ) ] + variable=RelObjectName() { result.setSkipVariable(variable); } + ) + ) + ] + ContextualKeyword("PATTERN") "(" pattern=RowPattern(0) ")" { result.setPattern(pattern); } + [ LOOKAHEAD({ isKeywordAhead("SUBSET") }) ContextualKeyword("SUBSET") subset=MatchRecognizeSubset() + { result.getSubsets().add(subset); } + ( LOOKAHEAD(2) "," subset=MatchRecognizeSubset() { result.getSubsets().add(subset); } )* + ] + ContextualKeyword("DEFINE") definition=MatchRecognizeDefinition() { result.getDefinitions().add(definition); } + ( LOOKAHEAD(2) "," definition=MatchRecognizeDefinition() { result.getDefinitions().add(definition); } )* + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) ContextualKeyword("OPTIONS") "(" + { options = new MatchRecognize.Options(); } + [ LOOKAHEAD({ isKeywordAhead("use_longest_match") }) ContextualKeyword("use_longest_match") "=" + ( value= | value= ) { options.setUseLongestMatch(value.kind == K_TRUE); } + ] + ")" { result.setOptions(options); } + ] + ")" + { + if (Dialect.BIGQUERY.name().equals(getAsString(Feature.dialect))) { + if (result.getOrderByElements().isEmpty() || result.getMeasures().isEmpty()) { + throw new ParseException("BigQuery MATCH_RECOGNIZE requires ORDER BY and MEASURES"); + } + if (result.getRowsPerMatch() != null || result.getSkipVariable() != null || !result.getSubsets().isEmpty()) { + throw new ParseException("BigQuery does not support ROWS PER MATCH, SKIP TO a variable or SUBSET"); + } } - ) - )* //zero or many times those productions - { - return options; - } + linkAST(result, jjtThis); return result; + } } -UseStatement Use(): { +MatchRecognize.Subset MatchRecognizeSubset() #MatchRecognizeSubset: +{ String name; - boolean hasSchemaKeyword = false; + String variable; + List variables = new ArrayList(); } { - [ LOOKAHEAD(2) { hasSchemaKeyword = true; } ] name = RelObjectName() + name=RelObjectName() "=" "(" variable=RelObjectName() { variables.add(variable); } + ( "," variable=RelObjectName() { variables.add(variable); } )* ")" { - return new UseStatement(name, hasSchemaKeyword); + MatchRecognize.Subset result = new MatchRecognize.Subset(name, variables); + linkAST(result, jjtThis); return result; } } -Statement Show(): +MatchRecognize.Definition MatchRecognizeDefinition() #MatchRecognizeDefinition: { - Statement statement; - List captureRest; + String name; + Expression expression; } { - + name=RelObjectName() expression=Expression() + { + MatchRecognize.Definition result = new MatchRecognize.Definition(name, expression); + linkAST(result, jjtThis); + return result; + } +} + +RowPattern RowPattern(int depth) #RowPattern: +{ + List alternatives = new ArrayList(); + RowPattern pattern; + Token separator; +} +{ + { + if (depth > getAsLong(Feature.allowedNestingDepth)) { + throw new ParseException("Row pattern exceeds the allowed nesting depth"); + } + } ( - LOOKAHEAD(2) statement = ShowColumns() - | - LOOKAHEAD(2) statement = ShowIndex() + LOOKAHEAD({ Dialect.SNOWFLAKE.name().equals(getAsString(Feature.dialect)) }) + pattern=SnowflakeRowPatternSequence(depth) { alternatives.add(pattern); } | - LOOKAHEAD(2) statement = ShowTables() - | - // any of the RDBMS specific SHOW syntax - captureRest = captureRest() - { - if (captureRest.size()==1) { - statement = new ShowStatement(captureRest.get(0)); - } else { - statement = new UnsupportedStatement("SHOW", captureRest); - } - } + pattern=RowPatternSequence(depth) { alternatives.add(pattern); } + ( ( separator= | separator= ) + { if (separator.kind == OP_CONCAT) { alternatives.add(new RowPattern.Empty()); } } + pattern=RowPatternSequence(depth) { alternatives.add(pattern); } + )* ) { - return statement; + RowPattern result = alternatives.size() == 1 ? alternatives.get(0) + : new RowPattern.Operation(RowPattern.Operation.Type.ALTERNATION, alternatives); + linkAST(result, jjtThis); + return result; } } -ShowColumnsStatement ShowColumns(): { - String tableName; +RowPattern SnowflakeRowPatternSequence(int depth) #SnowflakeRowPatternSequence: +{ + List patterns = new ArrayList(); + RowPattern pattern; } { - tableName = RelObjectName() + ( LOOKAHEAD({ isRowPatternPrimaryAhead() }) pattern=SnowflakeRowPatternAlternative(depth) { patterns.add(pattern); } )* { - return new ShowColumnsStatement(tableName); + RowPattern result = patterns.isEmpty() ? new RowPattern.Empty() + : patterns.size() == 1 ? patterns.get(0) + : new RowPattern.Operation(RowPattern.Operation.Type.SEQUENCE, patterns); + linkAST(result, jjtThis); return result; } } -ShowIndexStatement ShowIndex(): { - String tableName; +RowPattern SnowflakeRowPatternAlternative(int depth) #SnowflakeRowPatternAlternative: +{ + List patterns = new ArrayList(); + RowPattern pattern; } { - tableName = RelObjectName() + pattern=RowPatternFactor(depth) { patterns.add(pattern); } + ( pattern=RowPatternFactor(depth) { patterns.add(pattern); } )* { - return new ShowIndexStatement(tableName); + RowPattern result = patterns.size() == 1 ? patterns.get(0) + : new RowPattern.Operation(RowPattern.Operation.Type.ALTERNATION, patterns); + linkAST(result, jjtThis); return result; } } -Statement RefreshMaterializedView(): { - Table view = null; - boolean concurrently = false; - RefreshMode refreshMode = null; - List captureRest; +RowPattern RowPatternSequence(int depth): +{ + List patterns = new ArrayList(); + RowPattern pattern; + Token first = getToken(1); } { - - [ LOOKAHEAD(2) { concurrently = true; } ] - view = Table() - [ - { refreshMode = RefreshMode.WITH_DATA; } - [ - { refreshMode = RefreshMode.WITH_NO_DATA; } - ] - - ] - captureRest = captureRest() + ( LOOKAHEAD({ isRowPatternPrimaryAhead() }) pattern=RowPatternFactor(depth) { patterns.add(pattern); } )* { - if (concurrently && refreshMode == RefreshMode.WITH_NO_DATA) { - return new UnsupportedStatement("REFRESH", captureRest); - } else { - return new RefreshMaterializedViewStatement(view, concurrently, refreshMode); + if (patterns.isEmpty()) { return new RowPattern.Empty(); } + if (patterns.size() == 1) { return patterns.get(0); } + RowPattern result = new RowPattern.Operation(RowPattern.Operation.Type.SEQUENCE, patterns); + linkAST(result, first); + return result; + } +} + +RowPattern RowPatternFactor(int depth) #RowPatternFactor: +{ + RowPattern pattern; + String name; + List permutations = new ArrayList(); + Token first = getToken(1); +} +{ + ( + LOOKAHEAD({ isKeywordAhead("PERMUTE") && getToken(2).kind == OPENING_BRACKET }) + ContextualKeyword("PERMUTE") "(" + pattern=RowPattern(depth + 1) { permutations.add(pattern); } + ( "," pattern=RowPattern(depth + 1) { permutations.add(pattern); } )* ")" + { pattern = new RowPattern.Permute(permutations); } + | "{" "-" pattern=RowPattern(depth + 1) "-" "}" { pattern = new RowPattern.Exclusion(pattern); } + | { pattern = new RowPattern.Anchor(RowPattern.Anchor.Type.START); } + | LOOKAHEAD({ "$".equals(getToken(1).image) }) + { pattern = new RowPattern.Anchor(RowPattern.Anchor.Type.END); } + | "(" pattern=RowPattern(depth + 1) ")" { pattern = new RowPattern.Group(pattern); } + | name=RelObjectName() { pattern = new RowPattern.Variable(name); } + ) + { + if (Dialect.BIGQUERY.name().equals(getAsString(Feature.dialect)) + && (pattern instanceof RowPattern.Permute || pattern instanceof RowPattern.Exclusion)) { + throw new ParseException("BigQuery does not support PERMUTE or row pattern exclusion"); } } + [ LOOKAHEAD({ !(pattern instanceof RowPattern.Anchor) + && ("?".equals(getToken(1).image) || "*".equals(getToken(1).image) + || "+".equals(getToken(1).image) || getToken(1).kind == OPENING_CURLY_BRACKET) }) + { linkAST(pattern, first); } + pattern=RowPatternQuantifier(pattern) + ] + { linkAST(pattern, jjtThis); return pattern; } } -// https://dev.mysql.com/doc/refman/8.0/en/show-tables.html -ShowTablesStatement ShowTables(): { - ShowTablesStatement showTablesStatement; - EnumSet modifiers = EnumSet.noneOf(ShowTablesStatement.Modifiers.class); - ShowTablesStatement.SelectionMode selectionMode = null; - String dbName = null; - Expression likeExpression = null; - Expression whereCondition = null; + +RowPattern RowPatternQuantifier(RowPattern pattern) #RowPatternQuantifier: +{ + RowPattern.Quantified result; + Expression lower = null; + Expression upper = null; + boolean range = false; } { - [ { modifiers.add(ShowTablesStatement.Modifiers.EXTENDED); } ] - [ { modifiers.add(ShowTablesStatement.Modifiers.FULL); } ] - - [ - LOOKAHEAD(2) ( - { selectionMode = ShowTablesStatement.SelectionMode.FROM; } - | - { selectionMode = ShowTablesStatement.SelectionMode.IN; } + ( + "*" { result = new RowPattern.Quantified(pattern, RowPattern.Quantified.Type.ZERO_OR_MORE); } + | "+" { result = new RowPattern.Quantified(pattern, RowPattern.Quantified.Type.ONE_OR_MORE); } + | "?" { result = new RowPattern.Quantified(pattern, RowPattern.Quantified.Type.OPTIONAL); } + | "{" [ LOOKAHEAD({ !",".equals(getToken(1).image) }) lower=RowPatternBound() ] + [ "," { range = true; } [ LOOKAHEAD({ getToken(1).kind != CLOSING_CURLY_BRACKET }) upper=RowPatternBound() ] ] "}" + { + if (lower == null && upper == null) { throw new ParseException("Row pattern quantifier requires a bound"); } + result = new RowPattern.Quantified(pattern, range ? RowPattern.Quantified.Type.RANGE : RowPattern.Quantified.Type.EXACT) + .setLowerBound(lower).setUpperBound(upper); + } ) - dbName = RelObjectName() - ] - [ ( likeExpression = SimpleExpression() | whereCondition = Expression()) ] - { - showTablesStatement = new ShowTablesStatement(); - showTablesStatement.setModifiers(modifiers); - showTablesStatement.setSelectionMode(selectionMode); - showTablesStatement.setDbName(dbName); - showTablesStatement.setLikeExpression(likeExpression); - showTablesStatement.setWhereCondition(whereCondition); - return showTablesStatement; - } + [ LOOKAHEAD({ (result.getType() != RowPattern.Quantified.Type.EXACT + || !Dialect.BIGQUERY.name().equals(getAsString(Feature.dialect))) && "?".equals(getToken(1).image) }) "?" { result.setReluctant(true); } ] + { linkAST(result, jjtThis); return result; } } -Values Values(): { - ExpressionList expressions; -} { - ( | ) - expressions = ExpressionList() +Expression RowPatternBound(): +{ + Token value; + Expression result; + Token first = getToken(1); +} +{ + ( value= { result = new LongValue(value.image); } + | "?" { result = new JdbcParameter(++jdbcParameterIndex, false, "?"); } + | result=UserVariable() + ) + { linkASTIfMissing(result, first); return result; } +} +List JoinsList(): +{ + List joinsList = new ArrayList(); + Join join = null; +} +{ + ( LOOKAHEAD(2) join=JoinerExpression() { joinsList.add(join); } )+ { - return new Values(expressions); + return joinsList; } } -ReturningClause ReturningClause(): +JoinHint JoinHint(): { - Token keyword; - List outputAliases = null; - List> selectItems; - Object dataItem; - List dataItems = null; + Token token; } { - ( keyword= | keyword= ) - [ LOOKAHEAD(2) outputAliases = ReturningOutputAliasList() ] - selectItems = SelectItemsList() + ( + token = + | + token = + | + token = + | + token = + ) + { + return new JoinHint(token.image); + } +} - [ - - ( dataItem = Table() | dataItem = UserVariable() ) - { dataItems = new ArrayList(); dataItems.add(dataItem); } +JoinHint DorisJoinHint(): +{ + Token token; +} +{ + "[" + ( + LOOKAHEAD({ isKeywordAhead("SHUFFLE") }) token= + | + LOOKAHEAD({ isKeywordAhead("BROADCAST") }) token= + ) + "]" + { return new JoinHint(token.image, JoinHint.Position.AFTER_JOIN); } +} + +Join JoinerExpression() #JoinerExpression: +{ + Join join = new Join(); + FromItem right = null; + Expression onExpression = null; + Column tableColumn; + List columns = null; + KSQLJoinWindow joinWindow = null; + JoinHint joinHint = null; + Token tk = null; +} +{ + [ LOOKAHEAD({ isAsOfJoinAhead() }) { join.setAsOf(true); } ] + [ { join.setGlobal(true); } ] + [ { join.setAny(true); } | { join.setAll(true); } ] + [ { join.setNatural(true); } ] + [ ( - "," - ( dataItem = Table() | dataItem = UserVariable() ) { dataItems.add(dataItem); } - )* + { join.setLeft(true); } [ { join.setSemi(true); } | { join.setAnti(true); } | { join.setOuter(true); } | { join.setAny(true); } | { join.setAll(true); } ] + | + ( + { join.setRight(true); } + | + { join.setFull(true); } + ) [ { join.setSemi(true); } | { join.setAnti(true); } | { join.setOuter(true); } | { join.setAny(true); } | { join.setAll(true); } ] + | + { join.setInner(true); } + | + // DuckDB allows SEMI/ANTI without a LEFT prefix + LOOKAHEAD({ isSemiAntiJoinAhead() }) + ( { join.setSemi(true); } | { join.setAnti(true); } ) + ) + | + { join.setCross(true); } + | + { join.setOuter(true); } ] + [ { join.setArray(true); } ] + + ( + ( + [ joinHint=JoinHint() {join.setJoinHint(joinHint); } ] + + [ LOOKAHEAD("[", { Dialect.DORIS.name().equals(getAsString(Feature.dialect)) }) + { + if (joinHint != null) { + throw new ParseException("Only one join hint is allowed per JOIN"); + } + } + joinHint=DorisJoinHint() { join.setJoinHint(joinHint); } + ] + [ { join.setFetch(true); } ] + ) + | + "," { join.setSimple(true); } ( { join.setOuter(true); } )? + | + { join.setStraight(true); } + | + {join.setApply(true); } + ) + + right=FromItem() + + // DuckDB 2.0: JOIN products APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(...) + [ + LOOKAHEAD({ isApproxNearestAhead() }) + tk= { join.setApproxNearest(Long.valueOf(tk.image)); } + onExpression=Expression() { join.setSimilarity(onExpression); } + ] + [ + LOOKAHEAD(2) ( + [ joinWindow = KSQLJoinWindowClause() {join.setJoinWindow(joinWindow);} ] + ( onExpression=Expression() { join.addOnExpression(onExpression); } + ( LOOKAHEAD({ getToken(1).kind == K_ON && !isInsertOnConflictAhead() }) onExpression=Expression() { join.addOnExpression(onExpression); } )* + ) + | + ( + "(" tableColumn=Column() { columns = new ArrayList(); columns.add(tableColumn); } + ( "," tableColumn=Column() { columns.add(tableColumn); } ) * + ")" { join.setUsingColumns(columns); } + ) + ) + ] { - return new ReturningClause(keyword.image, selectItems, outputAliases, dataItems); + linkAST(join,jjtThis); + join.setFromItem(right); + return join; } + +} + +/** Shared duration parsing for JOIN, aggregation windows, and grace periods. */ +KSQLWindow.Duration KSQLDuration(): +{ + Token value; + Token unit; +} +{ + value= ( unit= | unit= ) + { return new KSQLWindow.Duration(Long.parseLong(value.image), KSQLWindow.TimeUnit.from(unit.image)); } +} + +KSQLWindow.Duration KSQLGracePeriod(): +{ + KSQLWindow.Duration duration; +} +{ + AccessKeyword("GRACE") AccessKeyword("PERIOD") duration=KSQLDuration() + { return duration; } } -ReturningReferenceType ReturningReferenceKind(): +KSQLJoinWindow KSQLJoinWindowClause(): { - String refName; - ReturningReferenceType refType; + KSQLJoinWindow window; + KSQLWindow.Duration duration; } { - refName = RelObjectName() - { - refType = ReturningReferenceType.from(refName); - if (refType == ReturningReferenceType.OLD) { - return ReturningReferenceType.OLD; - } else if (refType == ReturningReferenceType.NEW) { - return ReturningReferenceType.NEW; + ( + "(" window=JoinWindow() ")" + | + duration=KSQLDuration() { + window = new KSQLJoinWindow().withDuration(duration.getValue()) + .withTimeUnit(duration.getTimeUnit()).withUsingBrackets(false); } - throw new ParseException("Expected OLD or NEW but found: " + refName); - } + ) + [ LOOKAHEAD({ isKeywordAhead("GRACE") }) duration=KSQLGracePeriod() { window.setGracePeriod(duration); } ] + { return window; } } -ReturningOutputAlias ReturningOutputAliasDefinition(): +KSQLJoinWindow JoinWindow(): { - ReturningReferenceType refType; - String aliasName; + KSQLJoinWindow window = new KSQLJoinWindow(); + KSQLWindow.Duration before; + KSQLWindow.Duration after = null; } { - refType = ReturningReferenceKind() - - aliasName = RelObjectName() + before=KSQLDuration() + [ "," after=KSQLDuration() ] { - return new ReturningOutputAlias(refType, aliasName); + if (after == null) { + window.setDuration(before.getValue()); + window.setTimeUnit(before.getTimeUnit()); + } else { + window.setBeforeDuration(before.getValue()); + window.setBeforeTimeUnit(before.getTimeUnit()); + window.setAfterDuration(after.getValue()); + window.setAfterTimeUnit(after.getTimeUnit()); + window.setBeforeAfterWindow(true); + } + return window; } } -List ReturningOutputAliasList(): +KSQLWindow KSQLWindowClause(): { - List outputAliases = new ArrayList(); - ReturningOutputAlias outputAlias; + KSQLWindow window = new KSQLWindow(); + KSQLWindow.Duration size; + KSQLWindow.Duration advance; + KSQLWindow.Duration grace; } { - "(" - outputAlias = ReturningOutputAliasDefinition() { outputAliases.add(outputAlias); } + ( - "," - outputAlias = ReturningOutputAliasDefinition() { outputAliases.add(outputAlias); } - )* + "(" size=KSQLDuration() + "," advance=KSQLDuration() { + window.setHoppingWindow(true); + window.setAdvanceDuration(advance.getValue()); + window.setAdvanceTimeUnit(advance.getTimeUnit()); + } + | + "(" size=KSQLDuration() { window.setSessionWindow(true); } + | + "(" size=KSQLDuration() { window.setTumblingWindow(true); } + ) + [ "," grace=KSQLGracePeriod() { window.setGracePeriod(grace); } ] ")" { - return outputAliases; + window.setSizeDuration(size.getValue()); + window.setSizeTimeUnit(size.getTimeUnit()); + return window; } } -Update UpdateWithWithItems( List> withItems ): +Expression WhereClause(): { - Update update; + Expression retval = null; } { - update = Update() { update.setWithItemsList( withItems ); - return update; -} + retval=Expression() + { return retval; } } -Update Update(): +Expression PreWhereClause(): { - Update update = new Update(); - Table table = null; - List startJoins = null; - List> with = null; - List updateSets; - Expression where = null; - PreferringClause preferringClause = null; - FromItem fromItem = null; - List joins = null; - Limit limit = null; - List orderByElements; - boolean useColumnsBrackets = false; - ReturningClause returningClause; - Token tk = null; - UpdateModifierPriority modifierPriority = null; - boolean modifierIgnore = false; + Expression retval = null; +} +{ + retval=Expression() + { return retval; } +} - OutputClause outputClause = null; +OracleHierarchicalExpression OracleHierarchicalQueryClause(): +{ + OracleHierarchicalExpression result = new OracleHierarchicalExpression(); + Expression expr; + Token first = getToken(1); } { - { update.setOracleHint(getOracleHint()); } - [ LOOKAHEAD(2) { modifierPriority = UpdateModifierPriority.LOW_PRIORITY; }] - [ LOOKAHEAD(2) { modifierIgnore = true; }] - table=TableWithAliasAndMysqlIndexHint() [ startJoins=JoinsList() ] - updateSets = UpdateSets() { update.setUpdateSets(updateSets); } + ( + ( + expr=XorExpression() {result.setStartExpression(expr);} + [ { result.setNoCycle(true); } ] expr=XorExpression() + { + result.setConnectExpression(expr); + } + ) + | + ( + [ { result.setNoCycle(true); } ] expr=XorExpression() + { + result.setConnectExpression(expr); + result.setConnectFirst(true); + } + [ LOOKAHEAD(2) expr=XorExpression() {result.setStartExpression(expr);} ] + ) + ) + { + linkAST(result, first); + return result; + } +} - [ outputClause = OutputClause() {update.setOutputClause(outputClause); } ] +PreferringClause PreferringClause(): +{ + Expression result = null; +} +{ + + result=PreferenceTerm() - [ LOOKAHEAD(2) - fromItem=FromItem() - [ LOOKAHEAD(2) joins=JoinsList() ] ] + { + PreferringClause preferring = new PreferringClause(result); + return preferring; + } +} - [ where=WhereClause() { update.setWhere(where); } ] - [ preferringClause=PreferringClause() { update.setPreferringClause(preferringClause); } ] +Expression PreferenceTerm(): +{ + Expression retval = null; +} +{ + // recursively build preferenceterm inside Plus + // like Expression -> XorExpression -> OrExpression -> ... - [ orderByElements = OrderByElements() { update.setOrderByElements(orderByElements); } ] - [ limit = PlainLimit() { update.setLimit(limit); } ] - [ returningClause = ReturningClause() { update.setReturningClause(returningClause); } ] + retval=Plus() { - return update.withWithItemsList(with) - .withTable(table) - .withStartJoins(startJoins) - .withFromItem(fromItem) - .withJoins(joins) - .withModifierPriority(modifierPriority) - .withModifierIgnore(modifierIgnore); + return retval; } } -List UpdateSets(): +Expression Plus(): { - ArrayList updateSets = new ArrayList(); - UpdateSet updateSet; - Column tableColumn; - Expression valueExpression; + Expression left, right, result; +} +{ + left=PriorTo() { result = left; } + ( + LOOKAHEAD(2) + + right=PriorTo() + { + result = new Plus(left, right); + left = result; + } + )* + { + return result; + } +} - ExpressionList columns; - ExpressionListvalues; +Expression PriorTo(): +{ + Expression left, right, result; + Token first = getToken(1); + Token open; } { ( - ( - tableColumn=Column() "=" valueExpression=Expression() - { updateSets.add( new UpdateSet (tableColumn, valueExpression)); } - ) + LOOKAHEAD(3) left=PreferenceTermTerminal() | - ( - { updateSet = new UpdateSet(); updateSets.add(updateSet); } - columns = ParenthesedExpressionList() { updateSet.setColumns(columns); } - "=" - ( - LOOKAHEAD(3) valueExpression = ParenthesedSelect() { updateSet.setValues( new ExpressionList(valueExpression)); } - | - values = ParenthesedExpressionList() { updateSet.setValues(values); } - ) - ) + open="(" left=PreferenceTerm() ")" { left = new ParenthesedExpressionList(left); linkAST(left, open); } ) + { result = left; } ( - LOOKAHEAD(2) ( - "," - tableColumn=Column() "=" valueExpression=Expression() - { updateSets.add( new UpdateSet (tableColumn, valueExpression)); } + LOOKAHEAD(2) + ( + LOOKAHEAD(3) right=PreferenceTermTerminal() | - ( - { updateSet = new UpdateSet(); updateSets.add(updateSet); } - columns = ParenthesedExpressionList() { updateSet.setColumns(columns); } - "=" - ( - LOOKAHEAD(3) valueExpression = ParenthesedSelect() { updateSet.setValues( new ExpressionList(valueExpression)); } - | - values = ParenthesedExpressionList() { updateSet.setValues(values); } - ) - ) + open="(" right=PreferenceTerm() ")" { left = new ParenthesedExpressionList(right); linkAST(left, open); } ) + { + result = new PriorTo(left, right); + linkAST(result, first); + left = result; + } )* - { - return updateSets; + return result; } } -List Partitions(): +Expression PreferenceTermTerminal(): { - List partitions = new ArrayList(); - Column tableColumn; - Expression valueExpression = null; + Expression retval; } { ( - ( - tableColumn=Column() [ "=" valueExpression=Expression() ] - { partitions.add( new Partition (tableColumn, valueExpression)); } - ) + LOOKAHEAD(2) retval=HighExpression() + | + LOOKAHEAD(2) retval=LowExpression() + | + LOOKAHEAD(2) retval=Inverse() + | + retval=Condition() ) - ( - LOOKAHEAD(2) ( - "," - tableColumn=Column() [ "=" valueExpression=Expression() ] - { partitions.add( new Partition (tableColumn, valueExpression)); } - ) - )* + { + return retval; + } +} + +Expression HighExpression() #HighExpression: +{ + Expression result; +} +{ + + result=Expression() { - return partitions; + HighExpression high = new HighExpression(result); + linkAST(high,jjtThis); + return high; + } +} + +Expression LowExpression() #LowExpression: +{ + Expression result; +} +{ + + result=Expression() + + { + LowExpression low = new LowExpression(result); + linkAST(low,jjtThis); + return low; } } -Insert InsertWithWithItems( List> withItems ): +Expression Inverse() #Inverse: { - Insert insert; + Expression result; } { - insert = Insert() { insert.setWithItemsList( withItems ); - return insert; -} + + "(" result=PreferenceTerm() ")" + + { + Inverse inverse = new Inverse(result); + linkAST(inverse,jjtThis); + return inverse; + } } -Insert Insert(): +GroupByElement GroupByColumnReferences(): { - Insert insert = new Insert(); - Table table = null; - List> with = null; - Column tableColumn = null; - ExpressionList columns = new ExpressionList(); - List partitions = new ArrayList(); - Expression exp = null; - ReturningClause returningClause; - Select select = null; - Token tk = null; - InsertModifierPriority modifierPriority = null; - boolean modifierIgnore = false; - - List updateSets; - List duplicateUpdateSets; - - String name = null; - boolean useAs = false; - boolean useSet = false; - Alias rowAlias = null; - OutputClause outputClause = null; - - InsertConflictTarget conflictTarget = null; - InsertConflictAction conflictAction = null; - - InsertDuplicateAction duplicateAction = null; - Token multiInsertToken = null; - List oracleMultiInsertBranches = new ArrayList(); - OracleMultiInsertClause oracleMultiInsertClause = null; - OracleMultiInsertBranch oracleMultiInsertBranch = null; + Expression columnReference; + GroupByElement groupBy = new GroupByElement(); + Expression expr; + ExpressionList list; + Token token; } { - { insert.setOracleHint(getOracleHint()); } - - [ - LOOKAHEAD(2) (tk = | tk = | tk = ) - { - if (tk!=null) - modifierPriority = InsertModifierPriority.from(tk.image); - } + + // PostgreSQL: GROUP BY [ ALL | DISTINCT ] grouping_element [, ...] + [ LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && (getToken(1).kind == K_ALL || getToken(1).kind == K_DISTINCT) }) + ( { groupBy.setAll(true); } | { groupBy.setDistinct(true); } ) ] - [ LOOKAHEAD(2) { modifierIgnore = true; }] ( - LOOKAHEAD(2, ( | ) ( | )) - ( - (multiInsertToken = | multiInsertToken = ) { - insert.setOracleMultiInsert(true); - insert.setOracleMultiInsertFirst(multiInsertToken.kind == K_FIRST); - } - ( - { - oracleMultiInsertBranch = new OracleMultiInsertBranch(); - } - oracleMultiInsertClause = OracleMultiInsertClause() { - oracleMultiInsertBranch.addClause(oracleMultiInsertClause); - table = oracleMultiInsertClause.getTable(); - } - ( - oracleMultiInsertClause = OracleMultiInsertClause() { - oracleMultiInsertBranch.addClause(oracleMultiInsertClause); - } - )* - { - oracleMultiInsertBranches.add(oracleMultiInsertBranch); - } - | - ( - ( - oracleMultiInsertBranch = OracleMultiInsertWhenBranch() { - if (table == null && !oracleMultiInsertBranch.getClauses().isEmpty()) { - table = oracleMultiInsertBranch.getClauses().get(0).getTable(); - } - oracleMultiInsertBranches.add(oracleMultiInsertBranch); - } - )+ - [ - oracleMultiInsertBranch = OracleMultiInsertElseBranch() { - if (table == null && !oracleMultiInsertBranch.getClauses().isEmpty()) { - table = oracleMultiInsertBranch.getClauses().get(0).getTable(); - } - oracleMultiInsertBranches.add(oracleMultiInsertBranch); - } - ] - ) - ) - select = Select() { - insert.setOracleMultiInsertBranches(oracleMultiInsertBranches); - } + LOOKAHEAD(2) ( + + "(" + list = GroupingSet() { groupBy.addGroupingSet(list); } + ( LOOKAHEAD(2) "," list = GroupingSet() { groupBy.addGroupingSet(list); })* + ")" ) | ( - [ LOOKAHEAD(2) ( - { insert.setOverwrite(true); insert.setTableKeyword(true); } - | [ LOOKAHEAD(2) { insert.setTableKeyword(true); }] - ) - ] table=Table() - [ LOOKAHEAD(2) "(" partitions=Partitions() ")" ] - - [ LOOKAHEAD({ isAliasAhead() }) [ { useAs = true; } ] name=RelObjectName() { table.setAlias(new Alias(name,useAs)); }] - - [ LOOKAHEAD(2) "(" columns=ColumnList() ")" ] - - [ LOOKAHEAD(2) { insert.setOverriding(true); } ] - - [ outputClause = OutputClause() { insert.setOutputClause(outputClause); } ] - ( - { insert.setOnlyDefaultValues(true); } - | - ( - updateSets = UpdateSets() { insert.withSetUpdateSets(updateSets); useSet = true; } - ) + LOOKAHEAD({ getAsBoolean(Feature.allowLegacyMySqlGroupBy) + && "MYSQL".equals(getAsString(Feature.dialect)) }) + LegacyMySqlGroupByExpressions(groupBy) | - select = Select() + list = ExpressionList() { groupBy.setGroupByExpressions(list); } ) + ( + LOOKAHEAD(2) + "(" + list = GroupingSet() { groupBy.addGroupingSet(list); } + ( LOOKAHEAD(2) "," list = GroupingSet() { groupBy.addGroupingSet(list); })* + ")" + )? + [ LOOKAHEAD(2) { groupBy.setMysqlWithRollup(true); } ] + ) + ) + { + return groupBy; + } +} - [ LOOKAHEAD({ isAliasAhead() && (select instanceof Values || useSet) }) rowAlias = Alias() { - if (select instanceof Values) { - select.setAlias(rowAlias); - } else { - insert.setRowAlias(rowAlias); - } - } ] - - [ LOOKAHEAD(2) - duplicateAction = InsertDuplicateAction() { insert.setDuplicateAction(duplicateAction); } - ] +void LegacyMySqlGroupByExpressions(GroupByElement groupBy): +{} +{ + LegacyMySqlGroupByExpression(groupBy) + ( LOOKAHEAD(2) "," LegacyMySqlGroupByExpression(groupBy) )* +} - [ - - [ conflictTarget = InsertConflictTarget() ] - conflictAction = InsertConflictAction() { insert.withConflictTarget(conflictTarget).setConflictAction(conflictAction); } - ] +void LegacyMySqlGroupByExpression(GroupByElement groupBy): +{ + Expression expression; + Token direction; +} +{ + expression = Expression() { groupBy.getGroupByExpressionList().add(expression); } + [ LOOKAHEAD(2) (direction= | direction=) { + groupBy.setGroupBySortDirection(groupBy.getGroupByExpressionList().size() - 1, + direction.kind == K_ASC ? GroupByElement.SortDirection.ASC + : GroupByElement.SortDirection.DESC); + } ] +} - [ returningClause = ReturningClause() { insert.setReturningClause(returningClause); } ] - ) +ExpressionList GroupingSet(): +{ + ExpressionList list; + Expression expression; +} +{ + ( + LOOKAHEAD(2) list = ParenthesedExpressionList() + | + expression = SimpleExpression() { list = linkElements(new ExpressionList(expression)); } ) + { + return list; + } +} +Expression Having(): +{ + Expression having = null; +} +{ + having=Expression() { - if (!columns.isEmpty()) { - insert.setColumns(columns); - } - if (!partitions.isEmpty()) { - insert.setPartitions(partitions); - } - return insert.withWithItemsList(with) - .withSelect(select) - .withTable(table) - .withModifierPriority(modifierPriority) - .withModifierIgnore(modifierIgnore); + return having; } } -OracleMultiInsertClause OracleMultiInsertClause(): +Expression Qualify(): { - OracleMultiInsertClause clause = new OracleMultiInsertClause(); - Table clauseTable = null; - ExpressionList clauseColumns = new ExpressionList(); - Select clauseSelect = null; + Expression qualify = null; } { - clauseTable=Table() - [ LOOKAHEAD(2) "(" clauseColumns=ColumnList() ")" ] - clauseSelect = Select() + qualify=Expression() { - if (!clauseColumns.isEmpty()) { - clause.setColumns(clauseColumns); - } - return clause.withTable(clauseTable).withSelect(clauseSelect); + return qualify; } } -OracleMultiInsertBranch OracleMultiInsertWhenBranch(): +List OrderByElements(): { - Expression whenExpression = null; - List clauses = null; + List orderByList = new ArrayList(); + OrderByElement orderByElement = null; } { - whenExpression = Expression() - clauses = OracleMultiInsertClauseList() + [ ] orderByElement=OrderByElement() { orderByList.add(orderByElement); } + ( LOOKAHEAD(2) "," orderByElement=OrderByElement() { orderByList.add(orderByElement); } )* { - return new OracleMultiInsertBranch() - .withWhenExpression(whenExpression) - .withClauses(clauses); + return orderByList; } } -OracleMultiInsertBranch OracleMultiInsertElseBranch(): +OrderByElement OrderByElement(): { - List clauses = null; + OrderByElement orderByElement = new OrderByElement(); + Expression columnReference = null; + Token collateToken = null; + WithFill withFill = null; + Token first = getToken(1); } { - - clauses = OracleMultiInsertClauseList() + columnReference = Expression() + [ LOOKAHEAD(1, ) (collateToken= | collateToken=) { columnReference = new CollateExpression(columnReference, collateToken.image); } ] + [ LOOKAHEAD(2) ( | ( { orderByElement.setAsc(false); } )) { orderByElement.setAscDescPresent(true); } ] + [ LOOKAHEAD(2) + [ LOOKAHEAD(2) ( + { orderByElement.setNullOrdering(OrderByElement.NullOrdering.NULLS_FIRST); } + | + { orderByElement.setNullOrdering(OrderByElement.NullOrdering.NULLS_LAST); } + ) + ] + ] + [ LOOKAHEAD(2) withFill = WithFill() { orderByElement.setWithFill(withFill); } ] + [ LOOKAHEAD(2) { orderByElement.setMysqlWithRollup(true); } ] { - return new OracleMultiInsertBranch() - .withElseClause(true) - .withClauses(clauses); + orderByElement.setExpression(columnReference); + linkAST(orderByElement, first); + return orderByElement; } } -List OracleMultiInsertClauseList(): +WithFill WithFill(): { - List clauses = new ArrayList(); - OracleMultiInsertClause clause = null; + WithFill withFill = new WithFill(); + Expression fillExpression = null; } { - clause = OracleMultiInsertClause() { - clauses.add(clause); - } - ( - clause = OracleMultiInsertClause() { - clauses.add(clause); - } - )* - { - return clauses; - } + + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setFrom(fillExpression); } ] + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setTo(fillExpression); } ] + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setStep(fillExpression); } ] + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setStaleness(fillExpression); } ] + { return withFill; } } -InsertConflictTarget InsertConflictTarget(): +List InterpolateClause(): { - String indexColumnName; - ArrayList indexColumnNames = new ArrayList(); - Expression indexExpression = null; - Expression whereExpression = null; - String constraintName = null ; + List interpolateElements = new ArrayList(); + InterpolateElement interpolateElement = null; } { - ( - ( - "(" - indexColumnName = RelObjectNameExt() { indexColumnNames.add(indexColumnName); } - ( "," indexColumnName = RelObjectNameExt() { indexColumnNames.add(indexColumnName); } )* -// | -// ( -// "(" indexExpression = Expression() ")" -// ) - - ")" - [ whereExpression = WhereClause() ] - ) - | - ( - constraintName = RelObjectNameExt() - ) - ) - - { return new InsertConflictTarget(indexColumnNames, indexExpression, whereExpression, constraintName); } + + [ LOOKAHEAD(2) "(" + [ interpolateElement = InterpolateElement() { interpolateElements.add(interpolateElement); } + ( "," interpolateElement = InterpolateElement() { interpolateElements.add(interpolateElement); } )* + ] + ")" ] + { return interpolateElements; } } -InsertConflictAction InsertConflictAction(): +InterpolateElement InterpolateElement(): { - InsertConflictAction conflictAction; - Expression whereExpression = null; - List updateSets; + InterpolateElement interpolateElement = new InterpolateElement(); + Column column = null; + Expression expression = null; } { - ( - LOOKAHEAD(2) ( - { conflictAction = new InsertConflictAction( ConflictActionType.DO_NOTHING ); } - ) - | - ( - { conflictAction = new InsertConflictAction( ConflictActionType.DO_UPDATE ); } - updateSets = UpdateSets() { conflictAction.setUpdateSets(updateSets); } - [ whereExpression = WhereClause() ] - ) - ) - - { return conflictAction - .withWhereExpression(whereExpression); } + column = Column() { interpolateElement.setColumn(column); } + [ expression = Expression() { interpolateElement.setExpression(expression); } ] + { return interpolateElement; } } -InsertDuplicateAction InsertDuplicateAction(): +Expression OptionHintParameter(): { - InsertDuplicateAction duplicateAction; - Expression whereExpression = null; - List updateSets; + UserVariable userVariable = null; + Expression parameter = null; } { ( - LOOKAHEAD(2) ( - { duplicateAction = new InsertDuplicateAction( ConflictActionType.NOTHING ); } - ) + LOOKAHEAD({ getToken(1).kind == S_AT_IDENTIFIER && getToken(2).kind == K_UNKNOWN }) + userVariable = UserVariable() + { + return new Column(userVariable + " UNKNOWN"); + } | - ( - { duplicateAction = new InsertDuplicateAction( ConflictActionType.DO_UPDATE ); } - updateSets = UpdateSets() { duplicateAction.setUpdateSets(updateSets); } - [ whereExpression = WhereClause() ] - ) + parameter = Expression() { return parameter; } ) - - { return duplicateAction - .withWhereExpression(whereExpression); } } +OptionClause OptionClause(): +{ + OptionClause optionClause = new OptionClause(); + OptionHint optionHint = null; +} +{ + "(" optionHint = OptionHint() { optionClause.addOptionHint(optionHint); } + ( "," optionHint = OptionHint() { optionClause.addOptionHint(optionHint); } )* + ")" + { return optionClause; } +} -OutputClause OutputClause(): +OptionHint OptionHint(): { - List> selectItemList = null; - UserVariable tableVariable = null; - Table outputTable = null; - List columnList = null; + OptionHint optionHint = new OptionHint(); + String name = null; + Expression value = null; + Expression parameter = null; } { - - selectItemList = SelectItemsList() - [ + name = OptionHintName() { optionHint.setName(name); } + [ ( - tableVariable = UserVariable() + LOOKAHEAD(1, "=") "=" value = Expression() { optionHint.setValue(value); optionHint.setUseEquals(true); } | - outputTable = Table() + LOOKAHEAD(1, "(") "(" parameter = OptionHintParameter() { optionHint.addParameter(parameter); } + ( "," parameter = OptionHintParameter() { optionHint.addParameter(parameter); } )* + ")" + | + LOOKAHEAD({ isOptionHintValueAhead() }) value = Expression() { optionHint.setValue(value); } ) - [ - LOOKAHEAD(2) columnList = ColumnsNamesList() - ] ] - - { - return new OutputClause(selectItemList, tableVariable, outputTable, columnList); - } + { return optionHint; } } -Upsert Upsert(): +String OptionHintName(): { - Upsert upsert = new Upsert(); - Table table = null; - ExpressionList columns; - List updateSets; - - Select select = null; - List duplicateUpdateSets; - InsertDuplicateAction duplicateAction = null; Token tk = null; + String word = null; + StringBuilder name = new StringBuilder(); } { ( - { upsert.setUpsertType(UpsertType.UPSERT); } - | - { upsert.setUpsertType(UpsertType.REPLACE); } - | - ( - { upsert.setUpsertType(UpsertType.INSERT_OR_REPLACE); } - ) - ) - [ LOOKAHEAD(2) { upsert.setUsingInto(true); } ] - - table=Table() { upsert.setTable(table); } + LOOKAHEAD({ isOptionHintWordAhead() }) + ( tk= | word = NonReservedWord() + | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= ) + { + if (name.length() > 0) { name.append(' '); } + name.append(tk != null ? tk.image : word); + tk = null; + word = null; + } + )+ + { return name.toString(); } +} - [ LOOKAHEAD(2) columns = ParenthesedColumnList() { upsert.setColumns(columns); } ] - ( - ( - - updateSets = UpdateSets() { upsert.setUpdateSets(updateSets); } - ) - | - ( - select = Select() { upsert.setSelect(select); } - ) - ) +JdbcParameter JdbcParameter() : { + Token tk; + JdbcParameter retval; +} +{ + ( tk="?" | tk= ) + { retval = new JdbcParameter(++jdbcParameterIndex, false, tk.image); } - [ - - duplicateAction = InsertDuplicateAction() { upsert.setDuplicateAction(duplicateAction); } - ] + [ LOOKAHEAD(2) token = { retval.setUseFixedIndex(true); retval.setIndex(Integer.valueOf(token.image)); } ] - { - return upsert; - } + { return retval; } } -Delete DeleteWithWithItems( List> withItems ): + +Limit LimitWithOffset() #LimitWithOffset: { - Delete delete; + Limit limit = new Limit(); + Expression firstExpression; + Expression secondExpression; } { - delete = Delete() { delete.setWithItemsList( withItems ); - return delete; -} + + { Token outerLimitPercentToken = limitPercentToken; } + try { + { limitPercentToken = findLimitPercentToken(); } + ( + LOOKAHEAD(3) firstExpression = ParenthesedSelect() + | + firstExpression = Expression() + ) + } finally { + limitPercentToken = outerLimitPercentToken; + } + ( + // MySQL: LIMIT offset, row_count + "," + secondExpression = Expression() + { + limit.setOffset(firstExpression); + limit.setRowCount(secondExpression); + } + | + // PostgreSQL: LIMIT row_count; DuckDB: LIMIT 10% or LIMIT 10 PERCENT + { + limit.setRowCount(firstExpression); + } + [ + LOOKAHEAD({ getToken(1).kind == K_PERCENT + || "%".equals(getToken(1).image) && isLimitClauseEnd(getToken(2)) }) + ( "%" { limit.setPercentUnit("%"); } | { limit.setPercentUnit("PERCENT"); } ) + ] + ) + { + linkAST(limit, jjtThis); + return limit; + } } -Delete Delete(): +Limit PlainLimit() #PlainLimit: { - Delete delete = new Delete(); - Table table = null; - List
tables = new ArrayList
(); - List> with = null; - FromItem usingFromItem = null; - List usingFromItemList = new ArrayList(); - List joins = null; - Expression where = null; - PreferringClause preferringClause = null; - Limit limit = null; - List orderByElements; - boolean hasFrom = false; - Token tk = null; - DeleteModifierPriority modifierPriority = null; - boolean modifierIgnore = false; - boolean modifierQuick = false; - - ReturningClause returningClause; - OutputClause outputClause; + Limit limit = new Limit(); + Expression rowCountExpression; } { - { delete.setOracleHint(getOracleHint()); } - [ LOOKAHEAD(2) { modifierPriority = DeleteModifierPriority.LOW_PRIORITY; }] - [ LOOKAHEAD(2) { modifierQuick = true; }] - [ LOOKAHEAD(2) { modifierIgnore = true; }] - [LOOKAHEAD(4) (table=TableWithAlias() { tables.add(table); } - ("," table=TableWithAlias() { tables.add(table); } )* - [ outputClause = OutputClause() {delete.setOutputClause(outputClause); } ] - | ) { hasFrom = true; }] - - [ LOOKAHEAD(3) table=TableWithAlias() [ LOOKAHEAD(2) joins=JoinsList() ] ] - [ usingFromItem=FromItem() { usingFromItemList.add(usingFromItem); } - ("," usingFromItem=FromItem() { usingFromItemList.add(usingFromItem); } )*] - [where=WhereClause() { delete.setWhere(where); } ] - [preferringClause=PreferringClause() { delete.setPreferringClause(preferringClause);} ] - [orderByElements = OrderByElements() { delete.setOrderByElements(orderByElements); } ] - [limit=PlainLimit() {delete.setLimit(limit); } ] - - [ returningClause = ReturningClause() { delete.setReturningClause(returningClause); } ] + // mysql-postgresql-> LIMIT (row_count | ALL | NULL) + + ( + LOOKAHEAD(3) rowCountExpression = ParenthesedSelect() + | + rowCountExpression = Expression() + ) { - if (joins != null && joins.size() > 0) { - delete.setJoins(joins); - } - return delete.withWithItemsList(with) - .withTables(tables) - .withTable(table) - .withHasFrom(hasFrom) - .withUsingFromItemList(usingFromItemList) - .withModifierPriority(modifierPriority) - .withModifierIgnore(modifierIgnore) - .withModifierQuick(modifierQuick); + limit.setRowCount(rowCountExpression); + linkAST(limit,jjtThis); + return limit; } } -Statement Merge( List> with ) : { - Merge merge = new Merge(); - Table table; - FromItem fromItem; - Expression condition; - List operations; - OutputClause outputClause; +Offset Offset(): +{ + Offset offset = new Offset(); + Expression offsetExpression; } { - { merge.setOracleHint(getOracleHint()); } table=TableWithAlias() { merge.setTable(table); } - fromItem = FromItem() { merge.setFromItem(fromItem); } - condition = Expression() { merge.setOnCondition(condition); } - - operations = MergeOperations() { merge.setOperations(operations); } + ( + // postgresql-> OFFSET offset + // sqlserver-oracle-> OFFSET offset (ROW | ROWS) + + offsetExpression=Expression() { offset.setOffset( offsetExpression ); } - [ outputClause = OutputClause() { merge.setOutputClause(outputClause); } ] + [ LOOKAHEAD(2) ( { offset.setOffsetParam("ROWS"); } | { offset.setOffsetParam("ROW"); })] - { return merge.withWithItemsList(with); } + ) + { + return offset; + } } -List MergeOperations() : { - List operationsList = new ArrayList(); - MergeOperation operation; +Fetch Fetch(): +{ + Fetch fetch = new Fetch(); + Token token = null; + Expression expression; + List fetchParameters = new ArrayList(); } { ( - LOOKAHEAD(2) operation = MergeWhenMatched() { operationsList.add(operation); } + LOOKAHEAD(3) ( + ( { fetch.setFetchParamFirst(true); } | ) + ( + { fetch.addFetchParameter("ROWS"); } + | + { fetch.addFetchParameter("ROW"); } + ) + ( + { fetch.addFetchParameter("ONLY"); } + | + { fetch.addFetchParameter("WITH TIES"); } + ) + ) | - operation = MergeWhenNotMatched() { operationsList.add(operation); } - )* - { return operationsList; } -} + ( + ( { fetch.setFetchParamFirst(true); } | ) -MergeOperation MergeWhenMatched() : { - Expression predicate = null; - MergeOperation operation; -} -{ - - [ predicate = Expression() ] - - ( - operation = MergeDeleteClause(predicate) | operation = MergeUpdateClause(predicate) + // Expression is optional according to https://www.h2database.com/html/commands.html#select + expression = Expression() { fetch.setExpression(expression); } + [ { fetch.addFetchParameter("PERCENT"); } ] + ( + ( + { fetch.addFetchParameter("ROWS"); } + | + { fetch.addFetchParameter("ROW"); } + ) + + ( + { fetch.addFetchParameter("ONLY"); } + | + { fetch.addFetchParameter("WITH TIES"); } + ) + ) + ) ) - { return operation; } + { + return fetch; + } } -MergeOperation MergeDeleteClause(Expression predicate) : { - MergeDelete md = new MergeDelete().withAndPredicate(predicate); +WithIsolation WithIsolation(): +{ + WithIsolation withIsolation = new WithIsolation(); + Token token = null; + JdbcParameter jdbc; } { - { return md; } + + token= + { withIsolation.setIsolation(token.image); + return withIsolation; + } } -MergeOperation MergeUpdateClause(Expression predicate) : { - MergeUpdate mu = new MergeUpdate().withAndPredicate(predicate); - List updateSets; - Expression condition; +OptimizeFor OptimizeFor(): +{ + Token token; + LongValue value; } { - updateSets = UpdateSets() { mu.setUpdateSets(updateSets); } - [ condition = Expression() { mu.setWhereCondition(condition); }] - [ LOOKAHEAD(2) condition = Expression() { mu.setDeleteWhereCondition(condition); } ] - { return mu; } + token= + { + value = new LongValue(token.image); + return new OptimizeFor(value.getValue()); + } } -MergeInsert MergeInsertClause(Expression predicate) : { - MergeInsert mi = new MergeInsert(); - ExpressionList columns; - ExpressionList expList; - Expression condition; +// according to http://technet.microsoft.com/en-us/library/ms189463.aspx +Top Top() #Top: +{ + Top top = new Top(); + Token token = null; + Expression expr = null; + JdbcParameter jdbc = null; } { - - [ "(" columns = ColumnList() ")" + + ( + token= { top.setExpression(new LongValue(token.image)); } + | + jdbc = JdbcParameter() { top.setExpression(jdbc); } + | + ":" { top.setExpression(new JdbcNamedParameter()); } + [ LOOKAHEAD(2) token = { ((JdbcNamedParameter)top.getExpression()).setName(token.image); } ] + | + "(" + expr=SimpleExpression() { - mi.setColumns( new ParenthesedExpressionList(columns) ); + top.setExpression(expr); + top.setParenthesis(true); } - ] - "(" expList = SimpleExpressionList() ")" - { - mi.setValues( new ParenthesedExpressionList(expList) ); - } - - [ condition = Expression() { mi.setWhereCondition(condition); } ] - + ")" + ) + [ LOOKAHEAD(2) { top.setPercentage(true); } ] + [ LOOKAHEAD(2) { top.setWithTies(true); } ] { - mi.setAndPredicate(predicate); - return mi; + linkAST(top,jjtThis); + return top; } } -MergeOperation MergeWhenNotMatched() : { - MergeOperation operation; - Expression predicate = null; - MergeSide side = null; - Token sideToken; +// according to http://www-01.ibm.com/support/knowledgecenter/SSGU8G_12.1.0/com.ibm.sqls.doc/ids_sqs_0156.htm +Skip Skip(): +{ + Skip skip = new Skip(); + Token token = null; + JdbcParameter jdbc; } { - - [ sideToken = { - side = MergeSide.fromImage(sideToken.image); - } ] - [ predicate = Expression() ] - + ( - operation = MergeInsertClause(predicate) { ((MergeInsert) operation).setSide(side); } + token= { skip.setRowCount(Long.parseLong(token.image)); } | - operation = MergeUpdateClause(predicate) { ((MergeUpdate) operation).setSide(side); } + token= { skip.setVariable(token.image); } | - operation = MergeDeleteClause(predicate) { ((MergeDelete) operation).setSide(side); } + jdbc = JdbcParameter() { skip.setJdbcParameter(jdbc); } ) - { return operation; } + { + return skip; + } } -// table names seem to allow ":" delimiters, e.g. for Informix see #1134 -ObjectNames RelObjectNames() : { - String token = null; - Token delimiter = null; - List data = new ArrayList(); - List delimiters = new ArrayList(); -} { - token = RelObjectName() { data.add(token); } - ( - LOOKAHEAD (2) ( - ( delimiter = "..." { delimiters.add("."); data.add(null); delimiters.add("."); data.add(null); delimiters.add("."); } ) - | - ( delimiter = ".." { delimiters.add("."); data.add(null); delimiters.add("."); } ) - | - ( ( delimiter = "." | delimiter = ":" ) { delimiters.add(delimiter.image); } ) - ) - - token = RelObjectNameExt() { data.add(token); } - ) * - - { return new ObjectNames(data, delimiters); } +JAVACODE +OracleHint getOracleHint() { + OracleHint hint = null; + Token tok = getToken(1); + // Retrieve first comment (if any) prior next token + if (tok.specialToken != null) { + tok = tok.specialToken; + while (tok.specialToken != null) tok = tok.specialToken; + // Check if it matches Hint pattern? + if (OracleHint.isHintMatch(tok.image)) { + hint = new OracleHint(); + hint.setComment(tok.image); + linkAST(hint, tok, tok); + } + } + return hint; } -// column names do not allow ":" delimeters as those represent JSON `GET` operators -ObjectNames ColumnIdentifier() : { - String token = null; - Token delimiter = null; - List data = new ArrayList(); - List delimiters = new ArrayList(); -} { - token = RelObjectName() { data.add(token); } +First First(): +{ + First first = new First(); + Token token = null; + JdbcParameter jdbc; +} +{ ( - LOOKAHEAD (2) ( - ( delimiter = "..." { delimiters.add("."); data.add(null); delimiters.add("."); data.add(null); delimiters.add("."); } ) - | - ( delimiter = ".." { delimiters.add("."); data.add(null); delimiters.add("."); } ) - | - ( delimiter = "." { delimiters.add(delimiter.image); } ) - ) - - token = RelObjectNameExt() { data.add(token); } - ) * - - { return new ObjectNames(data, delimiters); } + { first.setKeyword(First.Keyword.FIRST); } + | + { first.setKeyword(First.Keyword.LIMIT); } + ) + ( + token= { first.setRowCount(Long.parseLong(token.image)); } + | + token= { first.setVariable(token.image); } + | + jdbc = JdbcParameter() { first.setJdbcParameter(jdbc); } + ) + { + return first; + } } -// See: http://technet.microsoft.com/en-us/library/ms187879%28v=sql.105%29.aspx -Column Column() #Column : + +Expression Expression() #Expression : { - ObjectNames data = null; - ArrayConstructor arrayConstructor = null; - Token tk = null; + Expression expression = null; + Token first; } -{ - data = ColumnIdentifier() - [ LOOKAHEAD(2) tk= ] - // @todo: we better should return a SEQUENCE instead of a COLUMN - [ LOOKAHEAD(2) "." { data.getNames().add("nextval"); } ] - - [ LOOKAHEAD(2) arrayConstructor = ArrayConstructor(false) ] +{ + { first = getToken(1); } + expression=Condition() + { expression = prattExpressionRest(expression, first, 2); } { - Column col = new Column(data.getNames(), data.getDelimiters()); - if (tk != null) { col.withCommentText(tk.image); } - if (arrayConstructor!=null) { - col.setArrayConstructor(arrayConstructor); - } - linkAST(col,jjtThis); - return col; + linkAST(expression,jjtThis); + return expression; } } -/* - * Unified identifier production for SQL object names. - * - * Accepts base identifier tokens (S_IDENTIFIER, S_QUOTED_IDENTIFIER, DATA_TYPE, - * date literals), any non-reserved keyword, and — guarded by the semantic - * check isReservedKeywordAsIdentifier() — reserved keywords that can serve as - * unquoted identifiers in the current parser position. - * - * NOTE: K_FROM, K_SELECT, and K_CURRENT are deliberately NOT included here - * to avoid FIRST-set pollution in JavaCC's choice resolution. They are only - * valid as identifiers in dotted-name continuations (after '.'), named - * parameters (after ':'), index columns, constraint names, and similar - * restricted contexts. Those call sites add the three tokens inline. - */ -String RelObjectName() : -{ Token tk = null; String result = null; } +Expression XorExpression(): +{ + Expression result; + Expression right; + Token first = getToken(1); +} { + result=AndChain() ( - /* ── Base identifier tokens ─────────────────────────────────────── */ - tk= - | tk= - | tk= - | tk= - | tk= - - /* ── Non-reserved keywords (range-guarded) ──────────────────────── */ - | LOOKAHEAD({ getToken(1).kind >= MIN_NON_RESERVED_WORD - && getToken(1).kind <= MAX_NON_RESERVED_WORD }) - result = NonReservedWord() - - /* ── Reserved keywords usable as identifiers (context-guarded) ─── */ - | LOOKAHEAD({ isReservedKeywordAsIdentifier() }) - ( tk= | tk= | tk= | tk= - | tk= | tk= - | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= - | tk= | tk= | tk= - | tk= | tk= | tk= | tk= - | tk= | tk= | tk= ) - ) - { return result != null ? result : tk.image; } + LOOKAHEAD(2) + ( + right=AndChain() { result = new OrExpression(result, right); linkAST(result, first); } + | + right=AndChain() { result = new XorExpression(result, right); linkAST(result, first); } + ) + )* + { return result; } } -/* - * Extended version of RelObjectName that additionally accepts FROM, SELECT, - * and CURRENT as unquoted identifiers. - * - * These three tokens are kept OUT of RelObjectName() to avoid polluting - * JavaCC's FIRST-set computation (which would cause choice conflicts in - * expression parsing, subselect detection, etc.). - * - * Used only in specific contexts where the old grammar's RelObjectNameExt2 - * was called: dotted-name continuations, index/constraint names, type names, - * named-parameter identifier chains, and join table references. - */ -String RelObjectNameExt() : -{ Token tk = null; String result = null; } +/** AND — higher precedence than OR/XOR */ +Expression AndChain(): { - ( result = RelObjectName() | tk= | tk= | tk= ) - { return tk != null ? tk.image : result; } + Expression result; + Expression right; + Token first = getToken(1); +} +{ + result=Condition() + ( + LOOKAHEAD(2) + ( + right=Condition() {{ + AndExpression a = new AndExpression(result, right); result = a; + linkAST(result, first); + }} + | + right=Condition() {{ + AndExpression a = new AndExpression(result, right); + a.setUseOperator(true); result = a; + linkAST(result, first); + }} + ) + )* + { return result; } } -Table Table() #TableName : +Expression Condition(): { - //String serverName = null, databaseName = null, schemaName = null, tableName = null; - ObjectNames data = null; - Token fileNameToken = null; - Table table; - String timeTravelStr = null; + Expression result; + Expression left; + // Leading NOT / ! prefixes, leftmost token wraps outermost (issue #1170). + Deque notTokens = null; + int oraclePrior = EqualsTo.NO_ORACLE_PRIOR; + int oracleJoin = EqualsTo.NO_ORACLE_JOIN; + Token prior = null; } { ( - data = RelObjectNames() [ LOOKAHEAD(2) timeTravelStr = TimeTravelBeforeAlias() ] + LOOKAHEAD({ getToken(1).kind == K_NOT || "!".equals(getToken(1).image) }) + ( | "!" ) { - table = new Table(data.getNames()); - table.setTimeTravel(timeTravelStr); + if (notTokens == null) { + notTokens = new ArrayDeque(); + } + notTokens.addFirst(getToken(0)); } + )* + ( + LOOKAHEAD( JdbcNamedParameter() ":", { + getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image) + && (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) }) + result=ExistsExpression() | - fileNameToken = - { - // don't split name parts - table = new Table(fileNameToken.image, false); + LOOKAHEAD(2, { !isExistsIdentifierAhead() }) + result=ExistsExpression() + | + // last alternative of the choice: entering it and letting the productions + // themselves reject is equivalent to scanning first, so skip the deep + // lookahead simulation (this path runs for every plain condition) + LOOKAHEAD({ !interrupted }) + // A choice rather than [...], so that enclosing syntactic lookaheads honour the guard + ( LOOKAHEAD({ isPriorOperatorAhead() }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; prior = getToken(0); } | {} ) + left=SimpleExpression() { result = left; } + + // Consume Oracle (+) once, before dispatching + [ + LOOKAHEAD("(" "+" ")", { getToken(1).kind == OPENING_BRACKET }) + "(" "+" ")" + { + oracleJoin = EqualsTo.ORACLE_JOIN_RIGHT; + if (left instanceof Column) { + ((Column) left).setOldOracleJoinSyntax(oracleJoin); widen(left, null, getToken(0)); + } + } + ] + + // Single guard: only enter if next token can start a condition suffix + [ + LOOKAHEAD({ isConditionSuffixAhead() }) + ( + LOOKAHEAD({ isComparisonOperatorAhead() }) + result = RegularConditionRHS(left, oracleJoin) + | LOOKAHEAD(2) result = OverlapsCondition(left) + | LOOKAHEAD(3) result=InExpression(left) + | LOOKAHEAD(3) result=ExcludesExpression(left) + | LOOKAHEAD(3) result=IncludesExpression(left) + | LOOKAHEAD(2) result=Between(left) + | LOOKAHEAD(2) result = MemberOfExpression(left) + | LOOKAHEAD(3) result=IsNullExpression(left) + | LOOKAHEAD(3) result=IsBooleanExpression(left) + | LOOKAHEAD(3) result=IsJsonExpression(left) + | LOOKAHEAD(3) result=IsUnknownExpression(left) + | LOOKAHEAD(2) result=LikeExpression(left) + | LOOKAHEAD(3) result=IsDistinctExpression(left) + | result=SimilarToExpression(left) + ) + // SQL:2016 6.39: a boolean test applies to any boolean primary, + // so postfix IS predicates chain onto a comparison or predicate + // result: "a > 0 IS TRUE", "a IS NULL IS NOT TRUE". + ( + LOOKAHEAD({ isPostfixIsTestAhead() }) + ( + LOOKAHEAD(3) result=IsBooleanExpression(result) + | LOOKAHEAD(3) result=IsUnknownExpression(result) + | LOOKAHEAD(3) result=IsNullExpression(result) + | LOOKAHEAD(3) result=IsJsonExpression(result) + ) + )* + ] + ) + { + if (oraclePrior == EqualsTo.ORACLE_PRIOR_START + && result instanceof SupportsOldOracleJoinSyntax) { + ((SupportsOldOracleJoinSyntax) result).setOraclePriorPosition(oraclePrior); + widen(result, prior, null); + } + while (notTokens != null && !notTokens.isEmpty()) { + Token notToken = notTokens.pollFirst(); + result = new NotExpression(result, "!".equals(notToken.image)); + linkAST(result, notToken); } + return result; + } +} + +Expression RegularConditionRHS(Expression leftExpression, int oracleJoinRight) #RegularCondition: +{ + Expression result = null; + Expression rightExpression; + int oracleJoin = EqualsTo.NO_ORACLE_JOIN; + int oraclePrior = EqualsTo.NO_ORACLE_PRIOR; + Token token; +} +{ + // Only consume (+) here if it wasn't already consumed by Condition + [ LOOKAHEAD("(" "+" ")") "(" "+" ")" { oracleJoin = EqualsTo.ORACLE_JOIN_RIGHT; } ] + + ( + LOOKAHEAD(2) + ">" { result = new GreaterThan(); } + | "<" { result = new MinorThan(); } + | "=" { result = new EqualsTo(); } + | "==" { result = new EqualsTo("=="); } + | token= { result = new GreaterThanEquals(token.image); } + | token= { result = new MinorThanEquals(token.image); } + | token= { result = new NotEqualsTo(token.image); } + | token= { result = new NotEqualsTo(token.image); } + | token= { result = new NotEqualsTo(token.image); } + | "*=" { result = new TSQLLeftJoin(); } + | "=*" { result = new TSQLRightJoin(); } + | token= { result = new DoubleAnd(); } + | token= { result = new Contains(); } + | token= { result = new ContainedBy(); } + | "@@" { result = new Matches(); } + | "~" { result = new RegExpMatchOperator(RegExpMatchOperatorType.MATCH_CASESENSITIVE); } + | "~*" { result = new RegExpMatchOperator(RegExpMatchOperatorType.MATCH_CASEINSENSITIVE); } + | "!~" { result = new RegExpMatchOperator(RegExpMatchOperatorType.NOT_MATCH_CASESENSITIVE); } + | "!~*" { result = new RegExpMatchOperator(RegExpMatchOperatorType.NOT_MATCH_CASEINSENSITIVE); } + | "@>" { result = new JsonOperator("@>"); } + | "<@" { result = new JsonOperator("<@"); } + | "?" { result = new JsonOperator("?"); } + | "?|" { result = new JsonOperator("?|"); } + | "?&" { result = new JsonOperator("?&"); } + | { result = new JsonOperator("||"); } + | "-" { result = new JsonOperator("-"); } + | "-#" { result = new JsonOperator("-#"); } ) + ( LOOKAHEAD({ isPriorOperatorAhead() }) rightExpression=ComparisonItem() { oraclePrior = EqualsTo.ORACLE_PRIOR_END; } + | rightExpression=ComparisonItem() ) + + [ LOOKAHEAD(2) "(" "+" ")" { oracleJoin = EqualsTo.ORACLE_JOIN_LEFT; } ] + { - linkAST(table,jjtThis); - return table; + BinaryExpression regCond = (BinaryExpression) result; + regCond.setLeftExpression(leftExpression); + regCond.setRightExpression(rightExpression); + + if (oracleJoin > 0) + ((SupportsOldOracleJoinSyntax) result).setOldOracleJoinSyntax(oracleJoin); + + if (oraclePrior != EqualsTo.NO_ORACLE_PRIOR) + ((SupportsOldOracleJoinSyntax) result).setOraclePriorPosition(oraclePrior); + } + + { + linkAST(result, jjtThis, leftExpression); + return result; } } -Table TableWithAlias(): +Expression OverlapsCondition(Expression leftExpression): { - Table table = null; - Alias alias = null; + ExpressionList right; + Token operator = getToken(1); } { - table=Table() - [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { table.setAlias(alias); }] - { return table; } + + right = ParenthesedExpressionList() + { + ExpressionList left; + if (leftExpression instanceof ExpressionList) { + left = (ExpressionList) leftExpression; + } else { + left = linkElements(new ExpressionList(leftExpression)); + } + OverlapsCondition result = new OverlapsCondition(left, right); + linkAST(result, firstToken(leftExpression, operator)); + return result; + } } -Table TableWithAliasAndMysqlIndexHint(): +Expression SQLCondition(): { - Table table = null; - Alias alias = null; - MySQLIndexHint indexHint = null; + Expression result; + Expression left; } { - table=Table() - [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { table.setAlias(alias); } ] - [ LOOKAHEAD(2) indexHint=MySQLIndexHint() { table.setHint(indexHint); } ] - { return table; } + ( + LOOKAHEAD( JdbcNamedParameter() ":", { + getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image) + && (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) }) + result=ExistsExpression() + | + LOOKAHEAD(2, { !isExistsIdentifierAhead() }) + result=ExistsExpression() + | + // last alternative of the choice: skip the redundant lookahead simulation + LOOKAHEAD({ !interrupted }) + left = SimpleExpression() { result = left; } + [ + LOOKAHEAD(2) ( + LOOKAHEAD({ getToken(1).kind == K_OVERLAPS }) result=OverlapsCondition(left) + | + LOOKAHEAD(3, {!interrupted}) result=InExpression(left) + | + LOOKAHEAD(3) result=ExcludesExpression(left) + | + LOOKAHEAD(3) result=IncludesExpression(left) + | + LOOKAHEAD(2) result=Between(left) + | + result = MemberOfExpression(left) + | + LOOKAHEAD(3) result=IsNullExpression(left) + | + LOOKAHEAD(3) result=IsBooleanExpression(left) + | + LOOKAHEAD(3) result=IsJsonExpression(left) + | LOOKAHEAD(3) result=IsUnknownExpression(left) + | + LOOKAHEAD(2) result=LikeExpression(left) + | + LOOKAHEAD(3) result=IsDistinctExpression(left) + | + result=SimilarToExpression(left) + ) + ] + ) + { return result; } } -Number Number(): +Expression InExpression(Expression leftExpression) #InExpression : { Token token; - Number number; + int oldOracleJoin = 0; + boolean usingNot = false; + boolean usingGlobal = false; + Expression rightExpression; } { + [ "(" "+" ")" { oldOracleJoin=EqualsTo.ORACLE_JOIN_RIGHT; } ] + + [ { usingGlobal=true; } ] + [ { usingNot=true; } ] + ( - token = { number = Double.valueOf(token.image); } + LOOKAHEAD(2) token= { rightExpression = stringValue(token); } | - token = { number = Long.valueOf(token.image); } + LOOKAHEAD({ isNestedSetOperationAhead() }) rightExpression = ParenthesedSelect() + | + rightExpression = PrimaryExpression() ) - { - return number; + InExpression inExpression = new InExpression(leftExpression, rightExpression) + .withOldOracleJoinSyntax(oldOracleJoin) + .withNot(usingNot) + .setGlobal(usingGlobal); + linkAST(inExpression, jjtThis, leftExpression); + return inExpression; } } -SampleClause SampleClause(): +Expression IncludesExpression(Expression leftExpression) #IncludesExpression : { Token token; - SampleClause sampleClause; - String keyword; - String method=null; - Number percentageArgument; - String percentageUnit = null; - boolean argumentInBrackets = true; - Number offsetArgument = null; - Number repeatArgument=null; - Number seedArgument=null; + Expression rightExpression; } { - ( - ( - // Oracle - token= { keyword = token.image; } - [ token= { method = token.image; } ] - ) - | - ( - // SQL:2016 compliant - token = { keyword = token.image; } - ( token = | token = ) { method = token.image; } - ) - | - ( - // Duck DB - { keyword = "USING SAMPLE"; } - ( token = | token = ) { method = token.image; } - ) - ) - - ( - "(" percentageArgument = Number() - [ - "%" { percentageUnit="%"; } - | - { percentageUnit="PERCENT"; } - | - { percentageUnit="ROWS"; } - ] - ")" - - [ LOOKAHEAD(2) "(" repeatArgument = Number() ")" ] - - [ LOOKAHEAD(2) "(" seedArgument = Number() ")" ] - | - percentageArgument = Number() { argumentInBrackets = false; } - [ LOOKAHEAD(2) offsetArgument = Number() ] - ) - + (rightExpression = ParenthesedExpressionList()) { - sampleClause = new SampleClause(keyword, method, percentageArgument, percentageUnit, repeatArgument, seedArgument); - sampleClause.setArgumentInBrackets(argumentInBrackets); - sampleClause.setOffsetArgument(offsetArgument); - return sampleClause; + IncludesExpression includesExpression = new IncludesExpression(leftExpression, rightExpression); + + linkAST(includesExpression, jjtThis, leftExpression); + return includesExpression; } } -Select SelectWithWithItems( List> withItems): +Expression ExcludesExpression(Expression leftExpression) #ExcludesExpression : { - Select select; + Token token; + Expression rightExpression; } { - select = Select() { select.setWithItemsList( withItems ); - return select; -} + (rightExpression = ParenthesedExpressionList()) + { + ExcludesExpression excludesExpression = new ExcludesExpression(leftExpression, rightExpression); + + linkAST(excludesExpression, jjtThis, leftExpression); + return excludesExpression; + } } -Select Select() #Select: +Expression Between(Expression leftExpression) : { - Select select = null; - List> with = null; - List orderByElements = null; - Limit limit = null; - Offset offset = null; - Fetch fetch = null; - WithIsolation withIsolation = null; - Alias alias = null; + Between result = new Between(); + Token operator = getToken(1); + Expression betweenExpressionStart = null; + Expression betweenExpressionEnd = null; } { - - [ with=WithList() ] - ( - LOOKAHEAD(3) select = FromQuery() - | - ( - ( - LOOKAHEAD(3) select = PlainSelect() - | - LOOKAHEAD(3) select = Values() + [ { result.setNot(true); }] + + [ + LOOKAHEAD(2) ( + { result.setUsingSymmetric(true); } | - LOOKAHEAD(3) select = ParenthesedSelect() [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() {select.setAlias(alias);} ] + { result.setUsingAsymmetric(true); } ) - [ LOOKAHEAD(2) select = FromQueryFromSelect(select) ] - [ LOOKAHEAD(2) select = SetOperationList(select) ] - - [ LOOKAHEAD( ) orderByElements = OrderByElements() { select.setOrderByElements(orderByElements); } ] + ] + ( + LOOKAHEAD({ isParenthesedSelectAhead() }) betweenExpressionStart = ParenthesedSelect() + | + betweenExpressionStart = SimpleExpression() + [ + LOOKAHEAD({ isComparisonOperatorAhead() }) + betweenExpressionStart = RegularConditionRHS(betweenExpressionStart, EqualsTo.NO_ORACLE_JOIN) + ] + [ + LOOKAHEAD("(" "+" ")") + "(" "+" ")" + { + if (betweenExpressionStart instanceof Column) { + ((Column) betweenExpressionStart).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); widen(betweenExpressionStart, null, getToken(0)); + } + } + ] + ) - [ LOOKAHEAD() limit=LimitWithOffset() {select.setLimit(limit);} ] - [ LOOKAHEAD() offset = Offset() { select.setOffset(offset);} ] - [ LOOKAHEAD() fetch = Fetch() { select.setFetch(fetch);} ] - [ LOOKAHEAD( ) withIsolation = WithIsolation() { select.setIsolation(withIsolation);} ] + + ( + LOOKAHEAD({ isParenthesedSelectAhead() }) betweenExpressionEnd = ParenthesedSelect() + | + betweenExpressionEnd = SimpleExpression() + [ + LOOKAHEAD({ isComparisonOperatorAhead() }) + betweenExpressionEnd = RegularConditionRHS(betweenExpressionEnd, EqualsTo.NO_ORACLE_JOIN) + ] + [ + LOOKAHEAD("(" "+" ")") + "(" "+" ")" + { + if (betweenExpressionEnd instanceof Column) { + ((Column) betweenExpressionEnd).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); widen(betweenExpressionEnd, null, getToken(0)); + } + } + ] ) - ) + { - linkAST(select, jjtThis); - return select.withWithItemsList(with); + result.setLeftExpression(leftExpression); + result.setBetweenExpressionStart(betweenExpressionStart); + result.setBetweenExpressionEnd(betweenExpressionEnd); + linkAST(result, firstToken(leftExpression, operator)); + return result; } } -FromQuery FromQuery() #FromQuery: +Expression LikeExpression(Expression leftExpression) #LikeExpression: { - FromQuery fromQuery; - FromItem fromItem; - List lateralViews = null; - List joins = null; - PipeOperator pipeOperator; + LikeExpression result = new LikeExpression(); + Expression rightExpression = null; + Expression escape; + Token token; } { - fromItem = FromItem() { fromQuery = new FromQuery(fromItem); } - [ LOOKAHEAD(2) lateralViews=LateralViews() { fromQuery.setLateralViews(lateralViews); } ] - [ LOOKAHEAD(2) joins=JoinsList() { fromQuery.setJoins(joins); } ] ( - LOOKAHEAD(2) "|>" pipeOperator = PipeOperator() { fromQuery.add(pipeOperator); } - )* - + ( + token = "~~" | token = "~~*" | token = "~~~" | token = "!~~" | token = "!~~*" + ) { result.setLikeOperator(token.image); } + | + [ { result.setNot(true); } ] + ( + token = + | token = + | token = + | token = + | token = + | token = + | token = + | token = + | token = + | token = + | token = + | token = + ) { result.setLikeKeyWord( LikeExpression.KeyWord.from(token.image)); } + ) + [ LOOKAHEAD(2) {result.setUseBinary(true); } ] + rightExpression=SimpleExpression() + [ + LOOKAHEAD("(" "+" ")") + "(" "+" ")" + { + if (rightExpression instanceof Column) { + ((Column) rightExpression).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); widen(rightExpression, null, getToken(0)); + } + } + ] + [ LOOKAHEAD(2) + ( + LOOKAHEAD(2) token = { result.setEscape( stringValue(token) ); } + | + escape=Expression() { result.setEscape(escape); } + ) + ] { - return fromQuery; + result.setLeftExpression(leftExpression); + result.setRightExpression(rightExpression); + linkAST(result, jjtThis, leftExpression); + return result; } } -FromQuery FromQueryFromSelect(Select select): +Expression SimilarToExpression(Expression leftExpression) #SimilarToExpression: { - FromQuery fromQuery; - FromItem fromItem; - PipeOperator pipeOperator; + SimilarToExpression result = new SimilarToExpression(); + Expression rightExpression = null; } { - "|>" pipeOperator = PipeOperator() - { - fromQuery = new FromQuery(select, false); - fromQuery.add(pipeOperator); - } - ( - LOOKAHEAD(2) "|>" pipeOperator = PipeOperator() - { fromQuery.add(pipeOperator); } - )* - + [ { result.setNot(true); } ] + + rightExpression=SimpleExpression() + [ + LOOKAHEAD("(" "+" ")") + "(" "+" ")" + { + if (rightExpression instanceof Column) { + ((Column) rightExpression).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); widen(rightExpression, null, getToken(0)); + } + } + ] + [ LOOKAHEAD(2) token= { result.setEscape((stringValue(token)).getValue()); }] { - return fromQuery; + result.setLeftExpression(leftExpression); + result.setRightExpression(rightExpression); + linkAST(result, jjtThis, leftExpression); + return result; } } -PipeOperator PipeOperator() #PipeOperator: +Expression IsDistinctExpression(Expression leftExpression) #IsDistinctExpression: { - PipeOperator operator; + IsDistinctExpression result = new IsDistinctExpression(); + Expression rightExpression = null; } { - ( - // SELECT covers also EXTEND, WINDOW and RENAME - operator = SelectPipeOperator() - | - operator = SetPipeOperator() - | - operator = DropPipeOperator() - | - LOOKAHEAD(2) operator = AsPipeOperator() - | - operator = WherePipeOperator() - | - operator = LimitPipeOperator() - | - LOOKAHEAD(2) operator = AggregatePipeOperator() - | - operator = OrderByPipeOperator() - | - // covers UNION, INTERSET, EXCEPT - operator = SetOperationPipeOperator() - | - operator = JoinPipeOperator() - | - operator = CallPipeOperator() - | - operator = TableSamplePipeOperator() - | - operator = PivotPipeOperator() - | - operator = UnPivotPipeOperator() - ) + [ { result.setNot(true); } ] + rightExpression=SimpleExpression() { - return operator; + result.setLeftExpression(leftExpression); + result.setRightExpression(rightExpression); + linkAST(result, jjtThis, leftExpression); + return result; } } -SelectPipeOperator SelectPipeOperator(): +Expression IsNullExpression(Expression leftExpression): { - Token operatorKeyToken; - Token modifierToken = null; - SelectPipeOperator selectPipeOperator; - SelectItem selectItem; + IsNullExpression result = new IsNullExpression(); + Token operator = getToken(1); } { ( - ( operatorKeyToken = [ LOOKAHEAD(2) ( modifierToken= | modifierToken= ) ] ) - | - operatorKeyToken = - | - operatorKeyToken = - | - operatorKeyToken = + [ { result.setNot(true); } ] { result.setUseIsNull(true); } + | { result.setUseIsNull(true); result.setUseNotNull(true); } + | [ { result.setNot(true); } ] ) - selectItem = SelectItem() - { selectPipeOperator = new SelectPipeOperator(operatorKeyToken.image, selectItem, modifierToken !=null ? modifierToken.image : null); } - - ( LOOKAHEAD(2) "," selectItem = SelectItem() { selectPipeOperator.add(selectItem); } )* - { - return selectPipeOperator; + result.setLeftExpression(leftExpression); + linkAST(result, firstToken(leftExpression, operator)); + return result; } } -WherePipeOperator WherePipeOperator(): +Expression IsBooleanExpression(Expression leftExpression): { - WherePipeOperator wherePipeOperator; - Expression expression; + IsBooleanExpression result = new IsBooleanExpression(); + Token operator = getToken(1); } { - expression = Expression() + ( + [ { result.setNot(true); } ] ( { result.setIsTrue(true); } | { result.setIsTrue(false); }) + ) + { - wherePipeOperator = new WherePipeOperator(expression); - return wherePipeOperator; + result.setLeftExpression(leftExpression); + linkAST(result, firstToken(leftExpression, operator)); + return result; } } -String OrderSuffix(): +Expression IsJsonExpression(Expression leftExpression): { - Token token =null; - String orderSuffix = null; + IsJsonExpression result = new IsJsonExpression(); + Token type; } { - ( token = | token = ) { orderSuffix = token.image; } - [ LOOKAHEAD(2) ( token = | token = ) { orderSuffix += " NULLS " + token.image; } ] - - { - return orderSuffix; - } + [ { result.setNot(true); } ] + [ LOOKAHEAD({ isKeywordAhead("VALUE") || isKeywordAhead("SCALAR") + || isKeywordAhead("ARRAY") || isKeywordAhead("OBJECT") }) + ( type= | type= | type= | type= ) + { result.setType(IsJsonExpression.Type.valueOf(type.image.toUpperCase(Locale.ROOT))); } ] + [ LOOKAHEAD(2) ( { result.setUniqueKeys(IsJsonExpression.UniqueKeys.WITH); } + | { result.setUniqueKeys(IsJsonExpression.UniqueKeys.WITHOUT); } ) + { result.setUseKeysKeyword(false); } + [ LOOKAHEAD(1, ) { result.setUseKeysKeyword(true); } ] ] + { result.setLeftExpression(leftExpression); return result; } } -AggregatePipeOperator AggregatePipeOperator(): +Expression IsUnknownExpression(Expression leftExpression): { - AggregatePipeOperator aggregatePipeOperator; - SelectItem selectItem; - Token token =null; - String orderSuffix = null; + IsUnknownExpression result = new IsUnknownExpression(); + Token operator = getToken(1); } { - - selectItem = SelectItem() [ LOOKAHEAD(2) orderSuffix = OrderSuffix() ] - { aggregatePipeOperator = new AggregatePipeOperator(selectItem, orderSuffix); orderSuffix=null; } - - ( LOOKAHEAD(2) "," selectItem = SelectItem() [ LOOKAHEAD(2) orderSuffix = OrderSuffix() ] - { aggregatePipeOperator.add(selectItem, orderSuffix); orderSuffix=null; } )* - - [ - LOOKAHEAD(2) [ { aggregatePipeOperator.setShorthandOrdering(true); } ] - selectItem = SelectItem() [ LOOKAHEAD(2) orderSuffix = OrderSuffix() ] - { aggregatePipeOperator.addGroupItem(selectItem, orderSuffix); orderSuffix=null; } - ( - LOOKAHEAD(2) "," selectItem = SelectItem() [ LOOKAHEAD(2) orderSuffix = OrderSuffix() ] - { aggregatePipeOperator.addGroupItem(selectItem, orderSuffix); orderSuffix=null; } - )* - ] + [ { result.setNot(true); } ] + ) { - return aggregatePipeOperator; + result.setLeftExpression(leftExpression); + linkAST(result, firstToken(leftExpression, operator)); + return result; } } -OrderByPipeOperator OrderByPipeOperator(): +Expression ExistsExpression(): { - OrderByPipeOperator orderByPipeOperator; - List orderByElements; + ExistsExpression result = new ExistsExpression(); + Expression rightExpression = null; + Token first = getToken(1); } { - orderByElements = OrderByElements() + rightExpression=SimpleExpression() { - orderByPipeOperator = new OrderByPipeOperator(orderByElements); - return orderByPipeOperator; + result.setRightExpression(rightExpression); + linkAST(result, first); + return result; } } -AsPipeOperator AsPipeOperator(): +Expression MemberOfExpression(Expression leftExpression): { - AsPipeOperator asPipeOperator; - Alias alias; + MemberOfExpression result; + Expression rightExpression = null; + Token operator = getToken(1); } { - alias = Alias() + rightExpression=Expression() { - asPipeOperator = new AsPipeOperator( alias.withUseAs(true) ); - return asPipeOperator; + result = new MemberOfExpression(leftExpression, rightExpression); + linkAST(result, firstToken(leftExpression, operator)); + return result; } } -JoinPipeOperator JoinPipeOperator(): +ExpressionList ExpressionList() #ExpressionList: { - JoinPipeOperator joinPipeOperator; - Join join; + ExpressionList expressionList; + Token first = getToken(1); } { - join = JoinerExpression() + // ComplexExpressionList() parses every list form, "()" and bracketed operands such as + // "( 1 + a ) / ( 1 + b )" included. + expressionList = ComplexExpressionList() { - joinPipeOperator = new JoinPipeOperator(join); - return joinPipeOperator; + // Avoid redundant ExpressionLists containing only one ParenthesedExpressionList + // return the Parenthesed Sub ExpressionList instead + // Same for ParenthesedExpressionList containing only 1 ExpressionList + + if ( expressionList.size() == 1 && expressionList.get(0) instanceof ExpressionList ) { + ExpressionList subList = (ExpressionList) expressionList.get(0); + if (expressionList instanceof ParenthesedExpressionList) { + if (!(subList instanceof ParenthesedExpressionList)) { + expressionList = new ParenthesedExpressionList(subList); + } + } else if (subList instanceof ParenthesedExpressionList) { + expressionList = new ParenthesedExpressionList(subList); + } else { + expressionList = subList; + } + } + linkASTIfMissing(expressionList, first); + return expressionList; } } -SetPipeOperator SetPipeOperator(): +ParenthesedExpressionList ParenthesedExpressionList(): { - SetPipeOperator setPipeOperator; - List updateSets; + ExpressionList expressions=new ExpressionList(); + Token first = getToken(1); } { - updateSets = UpdateSets() + "(" + [ LOOKAHEAD({ getToken(1).kind != CLOSING_BRACKET }) expressions = ComplexExpressionList() ] + ")" { - setPipeOperator = new SetPipeOperator(updateSets); - return setPipeOperator; + ParenthesedExpressionList result = new ParenthesedExpressionList(expressions); + linkAST(result, first); + return result; } } -DropPipeOperator DropPipeOperator(): +ExpressionList SimpleExpressionList(): { - DropPipeOperator dropPipeOperator; ExpressionList columns; + ExpressionList expressions = new ExpressionList(); + Expression expr; + Token first = getToken(1); } { - columns = ColumnList() + expr=SimpleExpression() { expressions.add(expr); } + ( + LOOKAHEAD({ !interrupted && getToken(1).kind == K_COMMA }) "," + ( + LOOKAHEAD( RelObjectName() "->", { !isJsonArrowDialect() } ) expr=LambdaExpression() + | + LOOKAHEAD({ !interrupted }) expr=SimpleExpression() + ) + { + expressions.add(expr); + } + )* { - dropPipeOperator = new DropPipeOperator(columns); - return dropPipeOperator; + linkAST(expressions, first); + return expressions; } } -LimitPipeOperator LimitPipeOperator(): + +ExpressionList ColumnList(): { - LimitPipeOperator limitPipeOperator; - Expression expression; + ExpressionList expressions = new ExpressionList(); + Column expr; + Token first = getToken(1); } { - expression = Expression() { limitPipeOperator = new LimitPipeOperator(expression); } - [ LOOKAHEAD(2) expression = Expression() { limitPipeOperator.setOffsetExpression(expression); } ] - + expr=Column() { expressions.add(expr); } + ( LOOKAHEAD(2) "," expr=Column() { expressions.add(expr); } )* { - return limitPipeOperator; + linkAST(expressions, first); + return expressions; } } -// see https://manticore-projects.com/SQL2016Parser/syntax_snapshot.html#corresponding-spec -String SetOperationModifier(): +ParenthesedExpressionList ParenthesedColumnList(): { - Token tk; - String modifier = ""; - String identifier; + ExpressionList expressions; + Token first = getToken(1); } { - ( - LOOKAHEAD(2) ( - [ ( tk= | tk="DISTINCT") { modifier+=tk.image; } ] - { modifier+= " BY NAME"; } - [ - "MATCHING" { modifier+= " MATCHING"; } - "(" - identifier = RelObjectName() { modifier+="(" + identifier; } - ("," identifier = RelObjectName() { modifier+=", " + identifier; })* - ")" { modifier+=")"; } - ] - ) - | - ( - [ { modifier+= " STRICT"; } ] - { modifier+= " CORRESPONDING"; } - [ (tk= | tk="DISTINCT") { modifier+=tk.image; } ] - [ - { modifier+= " BY"; }[ (tk= | tk="DISTINCT") { modifier+=tk.image; } ] - "(" - identifier = RelObjectName() { modifier+="(" + identifier; } - ("," identifier = RelObjectName() { modifier+=", " + identifier;})* - ")" { modifier+=")"; } - ] - ) - | - ( tk= | tk= ) { modifier+=tk.image; } - ) + "(" expressions = ColumnList() ")" { - return modifier; + ParenthesedExpressionList result = new ParenthesedExpressionList(expressions); + linkAST(result, first); + return result; } } -SetOperationPipeOperator SetOperationPipeOperator(): +ExpressionList ComplexExpressionList(): { - SetOperationPipeOperator setOperationPipeOperator = null; - SetOperationType setOperationType; - String modifier = null; - ParenthesedSelect select; + ExpressionList columns; + ExpressionList expressions = new ExpressionList(); + Expression expr; + Token first = getToken(1); } { ( - ( - [ modifier=SetOperationModifier() ] { setOperationType = SetOperationType.UNION; } - select = ParenthesedSelect() { setOperationPipeOperator = new SetOperationPipeOperator(select, setOperationType, modifier); } - ) + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=OracleNamedFunctionParameter() | - ( - [ modifier=SetOperationModifier() ] { setOperationType = SetOperationType.INTERSECT; } - select = ParenthesedSelect() { setOperationPipeOperator = new SetOperationPipeOperator(select, setOperationType, modifier); } - ) + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=PostgresNamedFunctionParameter() | - ( - [ modifier=SetOperationModifier() ] { setOperationType = SetOperationType.EXCEPT; } - select = ParenthesedSelect() { setOperationPipeOperator = new SetOperationPipeOperator(select, setOperationType, modifier); } - ) + // The semantic lookahead makes JavaCC enter Expression() with no scan of its first token. + LOOKAHEAD({ !interrupted }) expr=Expression() ) + { + expressions.add(expr); + } ( - LOOKAHEAD(2) "," select = ParenthesedSelect() { setOperationPipeOperator.add(select); } + LOOKAHEAD({ !interrupted && getToken(1).kind == K_COMMA }) "," + ( + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=OracleNamedFunctionParameter() + | + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=PostgresNamedFunctionParameter() + | + LOOKAHEAD( RelObjectName() "->", { !isJsonArrowDialect() } ) expr=LambdaExpression() + | + LOOKAHEAD({ !interrupted }) expr=Expression() + ) { expressions.add(expr); } )* { - return setOperationPipeOperator; + linkAST(expressions, first); + return expressions; } } -CallPipeOperator CallPipeOperator(): +// @Todo: Refactor this with proper SQL:2016 functions according to https://manticore-projects.com/SQL2016Parser/syntax.html#character-value-function +// substring(expr1 from expr2) +// substring(expr1 from expr2 for expr3) +// trim(expr1 from expr2) <-- Superceded by TrimFunction() below +// position(expr1 in expr2) +// overlay(expr1 placing expr2 from expr3) +// overlay(expr1 placing expr2 from expr3 for expr4) +// expr1 has already been consumed +NamedExpressionList NamedExpressionListExprFirst(): { - TableFunction tableFunction; - Alias alias=null; + NamedExpressionList retval = new NamedExpressionList(); + List expressions = new ArrayList(); + List names = new ArrayList(); + Expression expr1 = null; + Expression expr2 = null; + Expression expr3 = null; + Expression expr4 = null; + Token tk2 = null; + Token tk3 = null; + Token tk4 = null; } { - tableFunction = TableFunction() [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] + expr1=SimpleExpression() + (tk2=|tk2=|tk2=) + { + names.add(""); + expressions.add(expr1); + names.add(tk2.image); + } + ( + expr2=SimpleExpression() { expressions.add(expr2);} + ( + (tk3=|tk3=) + expr3=SimpleExpression() {names.add(tk3.image); expressions.add(expr3);} + ( + (tk4=) + expr4=SimpleExpression() {names.add(tk4.image); expressions.add(expr4);} + )? + )? + ) { - return new CallPipeOperator(tableFunction, alias); + retval.setNames(names); + retval.setExpressions(expressions); + return retval; } } +Expression ComparisonItem() : +{ + Expression retval = null; +} +{ + ( + LOOKAHEAD(6, { getToken(1).kind == K_ANY || getToken(1).kind == K_SOME || getToken(1).kind == K_ALL }) retval=AnyComparisonExpression() + | + LOOKAHEAD({ getToken(1).kind == K_ROW }) retval=RowConstructor() + | + retval=SimpleExpression() + ) + + { + return retval; + } +} -TableSamplePipeOperator TableSamplePipeOperator(): +Expression AnyComparisonExpression() : { - Token token; + AnyType anyType; + Select select; + Token first = getToken(1); } { - "(" ( token= | token= ) ")" + ( + { anyType = AnyType.ANY; } + | + { anyType = AnyType.SOME; } + | + { anyType = AnyType.ALL; } + ) + select = ParenthesedSelect() { - return new TableSamplePipeOperator( token.image ); + AnyComparisonExpression result = new AnyComparisonExpression(anyType, select); + linkAST(result, first); + return result; } } -PivotPipeOperator PivotPipeOperator(): +Expression SimpleExpression(): { - Function aggregateExpression; - Column inputColumn; - List> pivotColumns; - Alias alias = null; + Expression retval = null; + UserVariable user = null; + Token operation = null; + Token first; } { - "(" aggregateExpression=Function() - inputColumn=Column() - "(" pivotColumns = SelectItemsList() ")" - ")" - [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] + [ LOOKAHEAD( 5, { getToken(1).kind == S_AT_IDENTIFIER } ) user = UserVariable() ( operation = "=" | operation = ":=" ) ] + { first = getToken(1); } + retval=PrimaryExpression() + // PostgreSQL path operators bind below arithmetic and above comparisons. + // MySQL also gives | and & their own tiers below <<, >>, so the loop's + // entry floor drops to 2 there instead of the shared bitwise level 4. { - return new PivotPipeOperator(aggregateExpression, inputColumn, pivotColumns, alias); + int arithFloor = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 2 : 4; + retval = prattArithRest(retval, first, arithFloor); + } + { + if (user != null) { + VariableAssignment assignment = new VariableAssignment(); + assignment.setVariable(user); + assignment.setOperation(operation.image); + assignment.setExpression(retval); + linkAST(assignment, firstToken(user, first)); + return assignment; + } else + return retval; } } -UnPivotPipeOperator UnPivotPipeOperator(): +StringValue CharacterSetIntroducedString(): { - Column valuesColumn; - Column nameColumn; - List> pivotColumns; - Alias alias = null; + Token introducer; + Token literal; } { - "(" valuesColumn=Column() - nameColumn=Column() - "(" pivotColumns = SelectItemsList() ")" - ")" - [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] + introducer= literal= { - return new UnPivotPipeOperator(valuesColumn, nameColumn, pivotColumns, alias); + StringValue result = stringValue(literal).withPrefix(introducer.image); + linkAST(result, introducer, literal); + return result; } } -TableStatement TableStatement(): +/** Multiplicative/bitwise-shift — higher precedence */ +Expression MulChain(): { - Table table = null; - List orderByElements = null; - Limit limit = null; - Offset offset = null; - TableStatement tableStatement = new TableStatement(); -}{ - - table = Table() - { tableStatement.setTable(table); } - [ LOOKAHEAD( ) orderByElements = OrderByElements() { tableStatement.setOrderByElements(orderByElements); } ] - [ LOOKAHEAD() limit = LimitWithOffset() { tableStatement.setLimit(limit);} ] - [ LOOKAHEAD() offset = Offset() { tableStatement.setOffset(offset);} ] - { return tableStatement; } - /* Support operationList */ + Expression retval; + Expression right; +} +{ + retval=PrimaryExpression() + ( + LOOKAHEAD(2) + ( + "*" right=PrimaryExpression() {{ Multiplication r = new Multiplication(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=PrimaryExpression() {{ Division r = new Division(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + "%" right=PrimaryExpression() {{ Modulo r = new Modulo(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=PrimaryExpression() {{ net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=PrimaryExpression() {{ IntegerDivision r = new IntegerDivision(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + ) + )* + { return retval; } } -ParenthesedSelect ParenthesedSelect() #ParenthesedSelect: +/** Additive/concat/bitwise — lower precedence */ +Expression AddChain(): { - ParenthesedSelect parenthesedSelect = new ParenthesedSelect(); - Select select; + Expression retval; + Expression right; } { - "(" - select = Select() - ")" - { - linkAST(parenthesedSelect,jjtThis); - return parenthesedSelect.withSelect(select); - } + retval=MulChain() + ( + LOOKAHEAD(2) + ( + "+" right=MulChain() {{ Addition r = new Addition(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + "-" right=MulChain() {{ Subtraction r = new Subtraction(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=MulChain() {{ Concat r = new Concat(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + LOOKAHEAD(2) right=MulChain() {{ Concat r = new Concat(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=MulChain() {{ BitwiseOr r = new BitwiseOr(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + "&" right=MulChain() {{ BitwiseAnd r = new BitwiseAnd(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=MulChain() {{ BitwiseLeftShift r = new BitwiseLeftShift(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=MulChain() {{ BitwiseRightShift r = new BitwiseRightShift(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + ) + )* + { return retval; } } -ParenthesedInsert ParenthesedInsert() #ParenthesedInsert: -{ - ParenthesedInsert parenthesedInsert = new ParenthesedInsert(); - Insert insert; +Expression ArrayExpression(Expression obj): { + Expression expr; +} { + expr = ArraySubscript(obj) + ( LOOKAHEAD(2) expr = ArraySubscript(expr) )* + { return expr; } } -{ - "(" - insert = Insert() - ")" + +ArrayExpression ArraySubscript(Expression obj): { + Expression idxExpr = null; + RangeExpression slice = null; + Token opening; +} { + opening="[" + try { + { subscriptOpenings.push(opening); } + [ LOOKAHEAD(3, { !":".equals(getToken(1).image) }) idxExpr = ArrayIndexExpression() ] + [ slice = SliceTail(idxExpr) ] + } finally { + subscriptOpenings.pop(); + } + "]" { - return parenthesedInsert.withInsert(insert); + ArrayExpression result = slice == null + ? new ArrayExpression(obj, idxExpr, null, null) + : new ArrayExpression(obj, null, slice.getStartExpression(), + slice.getEndExpression()).withStepExpression(slice.getStepExpression()); + linkAST(result, firstToken(obj, opening)); + return result; } } -ParenthesedUpdate ParenthesedUpdate() #ParenthesedUpdate: -{ - ParenthesedUpdate parenthesedUpdate = new ParenthesedUpdate(); - Update update; +/** + * The rest of a slice after its optional start: {@code :[stop][:[step]]}. As in DuckDB, a + * {@code -} stop before a step stands for the end of the list, as in {@code l[1:-:2]}. + */ +RangeExpression SliceTail(Expression start): { + Expression stop = null; + Expression step = null; + Token colon = getToken(1); +} { + ":" + [ + LOOKAHEAD("-" ":") "-" + | + LOOKAHEAD({ !isSliceDelimiter(getToken(1)) }) stop = Expression() + ] + [ + ":" [ LOOKAHEAD({ !isSliceDelimiter(getToken(1)) }) step = Expression() ] + ] + { + RangeExpression result = new RangeExpression(start, stop).withStepExpression(step); + linkAST(result, firstToken(start, colon)); + return result; + } } -{ - "(" - update = Update() - ")" + +/** A subscript of a column, such as {@code col[1]}, {@code col[1, 2]} or {@code col[1:3]}. */ +ArrayConstructor Subscript() : { + ExpressionList expList = new ExpressionList(); + Expression exp; + Token opening; +} { + opening="[" + try { + { subscriptOpenings.push(opening); } + [ + exp=SubscriptElement() { expList.add(exp); } + ( "," exp=SubscriptElement() { expList.add(exp); } )* + ] + } finally { + subscriptOpenings.pop(); + } + "]" { - return parenthesedUpdate.withUpdate(update); + ArrayConstructor result = new ArrayConstructor(linkElements(expList), false); + linkAST(result, opening); + return result; } } -ParenthesedDelete ParenthesedDelete() #ParenthesedDelete: +Expression SubscriptElement(): { - ParenthesedDelete parenthesedDelete = new ParenthesedDelete(); - Delete delete; + Expression expression = null; } { - "(" - delete = Delete() - ")" - { - return parenthesedDelete.withDelete(delete); - } + ( + LOOKAHEAD({ ":".equals(getToken(1).image) }) expression=SliceTail(null) + | + expression=Expression() [ expression=SliceTail(expression) ] + ) + { return expression; } } -LateralView LateralView() #LateralView: +/** + * Array subscript index. BigQuery addresses array elements positionally with + * OFFSET(n), ORDINAL(n), SAFE_OFFSET(n) and SAFE_ORDINAL(n). Only OFFSET needs + * an explicit rule here, the others are plain identifiers and parse as functions. + */ +Expression ArrayIndexExpression() : { - boolean useOuter = false; - Function generatorFunction = null; - String tableName = null; - String columnName = null; - Alias tableAlias = null; - Alias columnAlias = null; + Token token; + Expression expr; } { - [ { useOuter=true; } ] - generatorFunction = Function() - [ LOOKAHEAD(2) - tableName=RelObjectName() + ( + LOOKAHEAD(2) token = "(" expr = SimpleExpression() ")" { - tableAlias = new Alias(tableName, false); + Function offset = new Function() + .withName(token.image) + .withParameters(linkElements(new ExpressionList(expr))); + linkAST(offset, token); + return offset; } - ] - columnName = RelObjectName() { columnAlias = new Alias(columnName, true); } - - // Spark SQL supports multiple Alias Columns: https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-lateral-view.html - // we simulate this by setting the alias name to null and then just adding the columns - [ - LOOKAHEAD(2) "," { columnAlias.setName(null); columnAlias.addAliasColumns( columnName); } - columnName = RelObjectName() { columnAlias.addAliasColumns( columnName); } - ( - LOOKAHEAD(2) "," columnName = RelObjectName() { columnAlias.addAliasColumns( columnName); } - )* - ] - { - return new LateralView( - useOuter - , generatorFunction - , tableAlias - , columnAlias - ); - } + | + expr = SimpleExpression() { return expr; } + ) } -ForClause ForClause() #ForClause: +Expression NavigationStep(Expression base): { - Token token = null; - ForClause forClause = new ForClause(); + Expression step = null; + String nm = null; + Function method; } { - ( - token = - | - token = + "." ( - ( - ( [ LOOKAHEAD(2) "(" ")" ] | ) - ( - LOOKAHEAD(2) "," - ( - - | - | [ LOOKAHEAD(2) "(" ")" ] - | - | [ LOOKAHEAD(2) "(" ")" ] - | [ LOOKAHEAD(2) ( | ) ] - ) - )* - ) - | - ( - - ( - LOOKAHEAD(2) "," - ( - - | - | [ LOOKAHEAD(2) "(" ")" ] - | - ) - )* - ) + LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) && isFunctionAhead() }) + method=Function() { step = new MethodCallExpression(base, method); } | - ( - [ LOOKAHEAD(2) "(" ")" ] - ( - LOOKAHEAD(2) "," - ( - - | - | [ LOOKAHEAD(2) "(" ")" ] - | [ LOOKAHEAD(2) ( | ) ] - ) - )* - ) + nm=RelObjectNameExt() { step = new RowGetExpression(base, nm); } ) | - ( - token = ( | ) - ( - LOOKAHEAD(2) "," - ( - [ LOOKAHEAD(2) "(" ")" ] - | - | - ) - )* - ) + step = ArrayExpression(base) ) - { - forClause.setForOption(token.image); - linkAST(forClause,jjtThis); - return forClause; - } + { return step; } } - -List LateralViews(): +StringValue CharacterLiteral(boolean link): { - ArrayList lateralViews = new ArrayList(); - LateralView lateralView = null; + Token literal; + StringValue value; + Token first = getToken(1); } { - lateralView = LateralView() { lateralViews.add(lateralView); } - ( LOOKAHEAD(2) lateralView = LateralView() { lateralViews.add(lateralView); } )* - + literal= { value = unlinkedStringValue(literal); } + ( LOOKAHEAD({ isAdjacentStringConcat() }) + literal= + { value.setValue(value.getValue() + unlinkedStringValue(literal).getValue()); } + )* { - return lateralViews; + // a primary expression links the literal itself, with its own tree node + if (link) { + linkAST(value, first); + } + return value; } } -LateralSubSelect LateralSubSelect() #LateralSubSelect: -{ - LateralSubSelect lateralSubSelect = new LateralSubSelect();; - Select select; -} +Expression PrimaryExpression() #PrimaryExpression: { - "(" select = Select() ")" { lateralSubSelect.withSelect(select).setPrefix("LATERAL"); } - { - linkAST(lateralSubSelect,jjtThis); - return lateralSubSelect; - } -} + Expression retval = null; + Expression nxt = null; + Expression expression = null; + CastExpression castExpr = null; + TimezoneExpression timezoneExpr = null; + Expression timezoneRightExpr = null; + Token token = null; + Token sign = null; + Token innerSign; + List innerSigns = null; + ColDataType type = null; + boolean not = false; + boolean exclamationMarkNot = false; + boolean dateExpressionAllowed = true; + Token coreFirst = null; + ExpressionList list; -PlainSelect PlainSelect() #PlainSelect: -{ - PlainSelect plainSelect = new PlainSelect(); - List> selectItems = null; - FromItem fromItem = null; - List lateralViews = null; - List joins = null; - List> distinctOn = null; - Expression preWhere = null; - Expression where = null; - ForClause forClause = null; - List orderByElements; - GroupByElement groupBy = null; - Expression having = null; - Expression qualify; - Limit limitBy = null; - Limit limit = null; - Offset offset = null; - Fetch fetch = null; - WithIsolation withIsolation = null; - OptimizeFor optimize = null; - Top top = null; - Skip skip = null; - First first = null; - OracleHierarchicalExpression oracleHierarchicalQueryClause = null; - PreferringClause preferringClause = null; - ExpressionList expressionList = null; - boolean partitionByBrackets = false; - List
intoTables = null; - MySqlSelectIntoClause mySqlSelectIntoClause = null; - Table updateTable = null; - List
updateTables = new ArrayList
(); - Wait wait = null; - boolean mySqlSqlCalcFoundRows = false; - Token token; - KSQLWindow ksqlWindow = null; - boolean noWait = false; - String windowName = null; - WindowDefinition winDef; - Table intoTempTable = null; - List settings = null; - Distinct distinct; + List> jsonIdents = null; } { - + [ { not=true; } | "!" { not=true; exclamationMarkNot=true; } ] + [ + ( sign="+" | sign="-" | sign="~" ) + // DuckDB's chained signs, e.g. - -1 or +-+1 + ( + LOOKAHEAD(1) ( innerSign="+" | innerSign="-" | innerSign="~" ) + { + if (innerSigns == null) { + innerSigns = new ArrayList(); + } + innerSigns.add(innerSign); + } + )* + ] + { coreFirst = getToken(1); } + ( + { retval = new NullValue(); } + + | LOOKAHEAD({!interrupted && isCaseExpressionAhead()}) retval=CaseWhenExpression() + + | LOOKAHEAD(2, {!interrupted}) retval=CharacterPrimary() + + | LOOKAHEAD({ !interrupted && getToken(1).kind == DATA_TYPE + && "MAP".equalsIgnoreCase(getToken(1).image) + && getToken(2).kind == OPENING_CURLY_BRACKET }) retval=MapExpression() + + | LOOKAHEAD({!interrupted && isImplicitCastAhead()}) retval=ImplicitCast() + + | retval = JdbcParameter() + + | LOOKAHEAD(2) retval =JdbcNamedParameter() + + | LOOKAHEAD(3) retval=UserVariable() + + | LOOKAHEAD(2, {!interrupted}) retval=NumericBind() + + | LOOKAHEAD(4 , {!interrupted}) retval=ExtractExpression() + + | LOOKAHEAD({ getToken(1).kind == K_XMLSERIALIZE && getToken(2).kind == OPENING_BRACKET }) + retval=XMLSerializeExpr() + + | LOOKAHEAD(3, { !interrupted}) retval = JsonFunction() + + | LOOKAHEAD(3, { !interrupted}) retval = JsonAggregateFunction() + + | LOOKAHEAD(3, { !interrupted}) retval = FullTextSearch() + + | LOOKAHEAD(2, {!interrupted}) retval= CastExpression() + + | LOOKAHEAD({ !interrupted && (isKeywordAhead("RUNNING") || getToken(1).kind == K_FINAL) + && getToken(3).kind == OPENING_BRACKET }) retval=RowPatternFunction() + + | LOOKAHEAD({ !interrupted && isFunctionAhead() }) + retval = Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ] + + | LOOKAHEAD(2, {!interrupted}) retval = IntervalExpression() { dateExpressionAllowed = false; } + + | token= { retval = new DoubleValue(token.image); } + + | token= { retval = new LongValue(token.image); } + + | token= { retval = new HexValue(token.image); } + + | LOOKAHEAD(3) retval=AllColumns(true) + + | LOOKAHEAD({ !interrupted && isAllTableColumnsAhead() }) + retval=AllTableColumns(true) + + // See issue #2207 + // there is a huge! performance deterioration from this production + //| LOOKAHEAD(FunctionAllColumns()) retval=FunctionAllColumns() + + // support timestamp expressions; current.x is a qualified column + | LOOKAHEAD({ !interrupted && (getToken(1).kind == K_TIME_KEY_EXPR + || getToken(1).kind == K_CURRENT && !".".equals(getToken(2).image)) }) + (token= | token=) { retval = new TimeKeyExpression(token.image); } + + | LOOKAHEAD(2, {!interrupted}) retval=DateTimeLiteralExpression() + + | LOOKAHEAD(3 , {!interrupted}) retval=StructType() + + | LOOKAHEAD(3, {!interrupted}) [ "<" type=ColDataType() ">" ] retval=ArrayConstructor(true) { if (type!=null) ((ArrayConstructor) retval).setDataType(type); } + + | LOOKAHEAD({ !interrupted && isListComprehensionAhead() }) retval=ListComprehension() + + | LOOKAHEAD(1, {!interrupted}) retval=ArrayConstructor(false) + + | LOOKAHEAD(2, {!interrupted}) retval = NextValExpression() + + | retval=ConnectByRootOperator() - [ { plainSelect.setMySqlHintStraightJoin(true); } ] + | LOOKAHEAD({ isPriorOperatorAhead() }) retval=ConnectByPriorOperator() - { plainSelect.setOracleHint(getOracleHint()); } + | LOOKAHEAD(2, { !interrupted && getToken(1).kind == K_KEY && getToken(2).kind != OPENING_BRACKET }) retval=KeyExpression() - [ LOOKAHEAD(2) skip = Skip() { plainSelect.setSkip(skip); } ] + | LOOKAHEAD({ !interrupted && getToken(1).kind == K_ALL }) { retval = new AllValue(); } - [ LOOKAHEAD(2) first = First() { plainSelect.setFirst(first); } ] + | LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() }) + retval=CharacterSetIntroducedString() - // Redshift allows TOP before DISTINCT - // https://docs.aws.amazon.com/redshift/latest/dg/r_SELECT_list.html - // @Todo: reflect the order when de-parsing - [ LOOKAHEAD(2) top = Top() { plainSelect.setTop(top); } ] + // DuckDB's lambda x, y : expr, before Column() which would take LAMBDA as a column name + | LOOKAHEAD({ !interrupted && isDuckDBLambdaAhead() }) retval=DuckDBLambdaExpression() + + | LOOKAHEAD(2, {!interrupted}) retval=Column() + [ + LOOKAHEAD( "(" "+" ")" ("," | ")") ) + "(" "+" ")" { ((Column) retval).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); widen(retval, null, getToken(0)); } + ] + + | LOOKAHEAD({ !interrupted && (getToken(1).kind == K_TRUE || getToken(1).kind == K_FALSE) }) + (token= | token=) { retval = new BooleanValue(token.image); } + + | retval=CharacterLiteral(false) + + | "{d" token= "}" { retval = new DateValue(token.image); } + + | "{t" token= "}" { retval = new TimeValue(token.image); } + + | "{ts" token= "}" { retval = new TimestampValue(token.image); } + + | LOOKAHEAD({ getAsBoolean(Feature.allowUnparenthesizedSubSelects) && !interrupted + && (getToken(1).kind == K_SELECT || getToken(1).kind == K_WITH || getToken(1).kind == K_VALUES + || (getToken(1).kind == OPENING_BRACKET && (getToken(2).kind == K_SELECT || getToken(2).kind == K_WITH || getToken(2).kind == K_VALUES))) }) retval=Select() + + | LOOKAHEAD({ !getAsBoolean(Feature.allowUnparenthesizedSubSelects) && !interrupted && isParenthesedSelectAhead() }) retval=ParenthesedSelect() + + | LOOKAHEAD({ !interrupted && isNestedSetOperationAhead() }) retval=ParenthesedSelect() - [ LOOKAHEAD(2) - ( - | ( - { distinct = new Distinct(); plainSelect.setDistinct(distinct); } - [ LOOKAHEAD(2) "ON" "(" distinctOn=SelectItemsList() { plainSelect.getDistinct().setOnSelectItems(distinctOn); } ")" ] - ) - | - { distinct = new Distinct(); distinct.setUseDistinctRow(true); plainSelect.setDistinct(distinct); } - | - { distinct = new Distinct(true); plainSelect.setDistinct(distinct); } - | - { plainSelect.setMySqlSqlCalcFoundRows(true); } - | - { plainSelect.setMySqlSqlCacheFlag(MySqlSqlCacheFlags.SQL_NO_CACHE); } - | - { plainSelect.setMySqlSqlCacheFlag(MySqlSqlCacheFlags.SQL_CACHE); } + list=ParenthesedExpressionList() + // Mutli-Variable Lambda Expression, e. g. + // SELECT map_filter(my_column, (k,v) -> v.my_inner_column = 'some_value') + // First-arg form (issue #2195): array_map((x,y,z) -> x + y, ...) + ( + LOOKAHEAD(2, { !isJsonArrowDialect() }) "->" + retval = Expression() + { + retval = LambdaExpression.from(list, retval); + } + | + { + if (list.size() == 1) { + retval = new ParenthesedExpressionList( (Expression) list.getExpressions().get(0)); + } else { + retval = list; + } + } + ) + + // PostgreSQL composite row expansion: (function()).* + // Re-enables FunctionAllColumns, which was disabled in #2207 due to the + // performance toll of a speculative syntactic lookahead. A bounded + // semantic follower check instead reads the already-parsed retval and + // peeks the next two tokens, avoiding any speculative production. + [ LOOKAHEAD( { isFunctionAllColumnsAhead(retval) } ) + "." "*" + { retval = new FunctionAllColumns(unwrapParenthesedFunction(retval)); } ] + + // Share field and method navigation with the other primary expressions. + ( LOOKAHEAD(1, ".") { linkASTIfMissing(retval, coreFirst); } nxt=NavigationStep(retval) { retval = nxt; } )* ) + ) + + [ + LOOKAHEAD(2, { !isColumnCollateAhead() }) { linkASTIfMissing(retval, coreFirst); } + (token= | token= | token=) { retval = new CollateExpression(retval, token.image); } ] [ - - ( - { plainSelect.setBigQuerySelectQualifier( PlainSelect.BigQuerySelectQualifier.AS_STRUCT ); } - | - { plainSelect.setBigQuerySelectQualifier( PlainSelect.BigQuerySelectQualifier.AS_VALUE ); } - ) + LOOKAHEAD(2, { dateExpressionAllowed } ) { linkASTIfMissing(retval, coreFirst); } retval = IntervalExpressionWithoutInterval(retval) ] - [ LOOKAHEAD(2) top = Top() { plainSelect.setTop(top); } ] + [ LOOKAHEAD(2) { linkASTIfMissing(retval, coreFirst); } retval = ArrayExpression(retval) ] - selectItems=SelectItemsList() + // navigation chains, e.g. Redshift SUPER col[0].field[1].sub: field accesses + // and subscripts may alternate freely after the primary expression + ( + LOOKAHEAD(2) { linkASTIfMissing(retval, coreFirst); } nxt = NavigationStep(retval) { retval = nxt; } + )* - [ LOOKAHEAD( ( | )) - mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.BEFORE_FROM) - { plainSelect.setMySqlSelectIntoClause(mySqlSelectIntoClause); } - ] - [ LOOKAHEAD() - intoTables = IntoClause() { plainSelect.setIntoTables(intoTables); } - ] - [ LOOKAHEAD(2) fromItem=FromItem() - [ LOOKAHEAD(2) lateralViews=LateralViews() ] - [ LOOKAHEAD(2) joins=JoinsList() ] - ] - [ LOOKAHEAD(3) { plainSelect.setUsingOnly(true); } fromItem=FromItem() - [ LOOKAHEAD(2) lateralViews=LateralViews() ] - [ LOOKAHEAD(2) joins=JoinsList() ] - ] + ( LOOKAHEAD(2) { linkASTIfMissing(retval, coreFirst); } "::" type=ColDataType() { + castExpr = new CastExpression(); + castExpr.setUseCastKeyword(false); + castExpr.setLeftExpression(retval); + castExpr.setColDataType(type); + retval=castExpr; + } + )* - // Clickhouse FINAL as shown at https://clickhouse.com/docs/en/operations/settings/settings#final - [ LOOKAHEAD(2) { plainSelect.setUsingFinal(true); } ] + // Check for JSON operands + [ + LOOKAHEAD(2, { !isPostgreSqlJsonAccess(getToken(1)) + && ((ternaryThenBranchDepth == 0 && mapKeyDepth == 0 && !isSliceColonAhead()) + || !":".equals(getToken(1).image)) }) { linkASTIfMissing(retval, coreFirst); } ( + LOOKAHEAD(2) ( + token="->" + | + token=":" + | + token="->>" + | + token="#>" + | + token="#>>" + ) - [ LOOKAHEAD(2) ksqlWindow=KSQLWindowClause() { plainSelect.setKsqlWindow(ksqlWindow); } ] - [ LOOKAHEAD(2) preWhere=PreWhereClause() { plainSelect.setPreWhere(preWhere); }] - [ LOOKAHEAD(2) where=WhereClause() { plainSelect.setWhere(where); }] - [ LOOKAHEAD(2) oracleHierarchicalQueryClause=OracleHierarchicalQueryClause() { plainSelect.setOracleHierarchical(oracleHierarchicalQueryClause); } ] - [ LOOKAHEAD(2) preferringClause=PreferringClause() { plainSelect.setPreferringClause(preferringClause); } - [LOOKAHEAD(2) ( - LOOKAHEAD(2) expressionList=ComplexExpressionList() + LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted }) expression=Expression() | - "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" + expression=SimpleExpression() ) { - preferringClause.setPartitionExpressionList(expressionList, partitionByBrackets); + if (jsonIdents == null) { + jsonIdents = new ArrayList>(); + } + jsonIdents.add(new AbstractMap.SimpleEntry(expression, token.image )); } - ] - ] - // Oracle supports "HAVING" before "GROUP BY", we will simply parse that but won't pay special attention to the order - [ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); }] - [ LOOKAHEAD(2) groupBy=GroupByColumnReferences() { plainSelect.setGroupByElement(groupBy); }] - [ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); }] - [ LOOKAHEAD(2) qualify=Qualify() {plainSelect.setQualify(qualify); }] - [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOracleSiblings(true); plainSelect.setOrderByElements(orderByElements); } ] - [ LOOKAHEAD(2) - windowName = RelObjectName() winDef = windowDefinition() { List winDefs = new ArrayList(); winDefs.add(winDef.withWindowName(windowName)); } - ( LOOKAHEAD(2) "," windowName = RelObjectName() winDef = windowDefinition() { winDefs.add(winDef.withWindowName(windowName)); } )* - { plainSelect.setWindowDefinitions(winDefs); } - ] - [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ] - [ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ] - [ LOOKAHEAD(2) { plainSelect.setEmitChanges(true); } ] - [ LOOKAHEAD(7) limit = LimitBy() { plainSelect.setLimitBy(limit); } ] - [ LOOKAHEAD() limit = LimitWithOffset() { plainSelect.setLimit(limit); } ] - [ LOOKAHEAD() offset = Offset() { plainSelect.setOffset(offset); } ] - [ LOOKAHEAD(, { limit==null }) limit = LimitWithOffset() { plainSelect.setLimit(limit); } ] - [ LOOKAHEAD() fetch = Fetch() { plainSelect.setFetch(fetch); } ] - [ LOOKAHEAD( ) withIsolation = WithIsolation() { plainSelect.setIsolation(withIsolation); } ] - [ LOOKAHEAD(2) - - ( - { plainSelect.setForMode(ForMode.UPDATE); } - | { plainSelect.setForMode(ForMode.SHARE); } - | ( { plainSelect.setForMode(ForMode.NO_KEY_UPDATE); }) - | ( { plainSelect.setForMode(ForMode.KEY_SHARE); }) - | ( { plainSelect.setForMode(ForMode.READ_ONLY); }) - | ( { plainSelect.setForMode(ForMode.FETCH_ONLY); }) - ) - [ LOOKAHEAD(2) - updateTable = Table() { updateTables.add(updateTable); } - ( LOOKAHEAD(2) "," updateTable = Table() { updateTables.add(updateTable); } )* - { plainSelect.setForUpdateTables(updateTables); } - ] - [ LOOKAHEAD() wait = Wait() { plainSelect.setWait(wait); } ] - [ LOOKAHEAD(2) ( { plainSelect.setNoWait(true); } - | { plainSelect.setSkipLocked(true); }) ] - [ LOOKAHEAD( ) orderByElements = OrderByElements() - { plainSelect.setOrderByElements(orderByElements); plainSelect.setForUpdateBeforeOrderBy(true); } - ] - ] - [ LOOKAHEAD( ( | )) - mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.TRAILING) - { plainSelect.setMySqlSelectIntoClause(mySqlSelectIntoClause); } + )+ + retval = JsonExpression(retval, jsonIdents) ] - [ LOOKAHEAD(2) settings = UpdateSets() { plainSelect.setSettings(settings); } ] - [ LOOKAHEAD() optimize = OptimizeFor() { plainSelect.setOptimizeFor(optimize); } ] - [ LOOKAHEAD(3) intoTempTable = Table() { plainSelect.setIntoTempTable(intoTempTable);} ] - [ LOOKAHEAD(3) { plainSelect.setUseWithNoLog(true); } ] + + ( LOOKAHEAD(2) { linkASTIfMissing(retval, coreFirst); } timezoneRightExpr=PrimaryExpression() { + if (timezoneExpr == null) + timezoneExpr = new TimezoneExpression(); + + timezoneExpr.addTimezoneExpression(timezoneRightExpr); + } + )* + { - plainSelect.setSelectItems(selectItems); - plainSelect.setFromItem(fromItem); - if ( lateralViews!=null && lateralViews.size()>0 ) { - plainSelect.setLateralViews( lateralViews ); + if (timezoneExpr != null && !timezoneExpr.getTimezoneExpressions().isEmpty()) { + timezoneExpr.setLeftExpression(retval); + retval=timezoneExpr; + // the field behind this getter is an ExpressionList + linkElements((ExpressionList) timezoneExpr.getTimezoneExpressions()); } - if ( joins!=null && joins.size()>0 ) { - plainSelect.setJoins( joins ); + // each value gets a range node only once something wraps it, the outermost takes jjtThis + Token valueFirst = coreFirst; + if (innerSigns != null) { + for (int i = innerSigns.size() - 1; i >= 0; i--) { + linkASTIfMissing(retval, valueFirst); + retval = new SignedExpression(innerSigns.get(i).image.charAt(0), retval); + valueFirst = innerSigns.get(i); + } } - linkAST(plainSelect,jjtThis); - return plainSelect; + if (sign != null) { + linkASTIfMissing(retval, valueFirst); + retval = new SignedExpression(sign.image.charAt(0), retval); + valueFirst = sign; + } + if (not) { + linkASTIfMissing(retval, valueFirst); + retval = new NotExpression(retval, exclamationMarkNot); + } + linkAST(retval, jjtThis); + return retval; + } +} + +/* https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/img_text/hierarchical_query_clause.html + + { CONNECT BY [ NOCYCLE ] condition [ START WITH condition ] + | START WITH condition CONNECT BY [ NOCYCLE ] condition + } + */ + +ConnectByRootOperator ConnectByRootOperator() #ConnectByRootOperator: { + Expression expression; +} +{ + expression = Expression() + { + return new ConnectByRootOperator(expression); + } +} + +ConnectByPriorOperator ConnectByPriorOperator() #ConnectByPriorOperator: { + Expression expression; +} +{ + expression = Expression() + { + return new ConnectByPriorOperator(expression); + } +} + +KeyExpression KeyExpression() #KeyExpression: { + Expression expression; +} +{ + expression = Expression() + { + return new KeyExpression(expression); + } +} + + +NextValExpression NextValExpression() : { + ObjectNames data = null; + Token token; +} +{ + token= data = RelObjectNames() + { + return new NextValExpression(data.getNames(), token.image); + } +} + +JdbcNamedParameter JdbcNamedParameter() : { + JdbcNamedParameter parameter = new JdbcNamedParameter(); + String name; + String namePart; +} +{ + (":" | "&" { parameter.setParameterCharacter("&"); } ) + name=IdentifierChain() + { + parameter.setName(name); + return parameter; + } +} + +OracleNamedFunctionParameter OracleNamedFunctionParameter() : { + Token token = null; + String name = null; + Expression expression; +} +{ + ( name=RelObjectNameExt() | token= ) + + expression=Expression() + { + return new OracleNamedFunctionParameter(name != null ? name : token.image, expression); } } -Select SetOperationList(Select select) #SetOperationList: { - SetOperationList list = new SetOperationList(); - List orderByElements = null; - Limit limit = null; - Offset offset = null; - Fetch fetch = null; - WithIsolation withIsolation = null; - List(); - List operations = new ArrayList(); - String modifier = null; +PostgresNamedFunctionParameter PostgresNamedFunctionParameter() : { + Token token = null; + String name = null; + Expression expression; } { - + ( name=RelObjectNameExt() | token= ) + + expression=Expression() { - selects.add(select); + return new PostgresNamedFunctionParameter(name != null ? name : token.image, expression); } +} - ( LOOKAHEAD(2) { modifier = null; } ( - ( - [ modifier=SetOperationModifier() ] { UnionOp union = new UnionOp(modifier); linkAST(union,jjtThis); operations.add(union); } - - ) - | - ( - [ modifier=SetOperationModifier() ] { IntersectOp intersect = new IntersectOp(modifier); linkAST(intersect,jjtThis); operations.add(intersect); } - ) - | - ( - [ modifier=SetOperationModifier() ] { MinusOp minus = new MinusOp(modifier); linkAST(minus,jjtThis); operations.add(minus); } - ) - | - ( - [ modifier=SetOperationModifier() ] { ExceptOp except = new ExceptOp(modifier); linkAST(except,jjtThis); operations.add(except); } - ) - - ) - - ( - select = PlainSelect() - | - select = Values() - | - select = ParenthesedSelect() - ) - { - selects.add(select); - } - )+ - - [ LOOKAHEAD(2) orderByElements=OrderByElements() {list.setOrderByElements(orderByElements);} ] - [ LOOKAHEAD() limit = LimitWithOffset() { list.setLimit(limit); } ] - [ LOOKAHEAD() offset = Offset() { list.setOffset(offset); } ] - [ LOOKAHEAD(, { limit==null }) limit = LimitWithOffset() { list.setLimit(limit); } ] - [ LOOKAHEAD() fetch = Fetch() { list.setFetch(fetch); } ] - [ LOOKAHEAD( ) withIsolation = WithIsolation() { list.setIsolation(withIsolation); } ] - +UserVariable UserVariable() : { + Token tk; + String varName; +} +{ + tk = varName=IdentifierChain2(tk.image) { - if ( selects.get(selects.size()-1) instanceof PlainSelect ) { - PlainSelect ps = (PlainSelect)selects.get(selects.size()-1); - if (ps.getOrderByElements() != null) { - list.setOrderByElements(ps.getOrderByElements()); - list.setLimit(ps.getLimit()); - list.setOffset(ps.getOffset()); - ps.setOrderByElements(null); - ps.setLimit(null); - ps.setOffset(null); - } - if (ps.getFetch() != null) { - list.setFetch(ps.getFetch()); - ps.setFetch(null); - } - if (ps.getIsolation() != null) { - list.setIsolation(ps.getIsolation()); - ps.setIsolation(null); - } - } - list.setBracketsOpsAndSelects(selects,operations); - return list; + UserVariable result = new UserVariable(varName); + linkAST(result, tk); + return result; } } -List> WithList(): +NumericBind NumericBind() : { + NumericBind var = new NumericBind(); + Token token; +} { - List> withItemsList = new ArrayList>(); - WithItem with = null; + ":" token= + { + var.setBindId(Integer.valueOf(token.image)); + return var; + } +} + +DateTimeLiteralExpression DateTimeLiteralExpression() : { + DateTimeLiteralExpression expr = new DateTimeLiteralExpression(); + Token t; } { - with=WithItem() { withItemsList.add(with); } - ( "," with=WithItem() { withItemsList.add(with); } )* + t= + { + expr.setType(DateTimeLiteralExpression.DateTime.from(t.image)); + } - { return withItemsList; } + ( t= | t= ) + { + expr.setValue(t.image); + return expr; + } } -WithItem WithItem() #WithItem: +RangeExpression RangeExpression(Expression startExpression): { - boolean recursive = false; - boolean materialized = false; - boolean usingNot = false; - String name = null; - List> selectItems = null; - WithFunctionDeclaration withFunctionDeclaration = null; - ParenthesedStatement statement = null; - WithSearchClause withSearchClause = null; - WithItem withItem; + Expression endExpression; + Token colon = getToken(1); } { - ( - LOOKAHEAD(2) - withFunctionDeclaration = WithFunctionDeclaration() - { - withItem = new WithItem().withWithFunctionDeclaration(withFunctionDeclaration); - } - | - ( - [ LOOKAHEAD(2) { recursive = true; } ] - name=RelObjectName() - [ "(" selectItems=SelectItemsList() ")" ] - - [ LOOKAHEAD(2) [ { usingNot = true; } ] { materialized = true; } ] - ( - LOOKAHEAD(2) statement = ParenthesedSelect() - | - LOOKAHEAD(2) statement = ParenthesedInsert() - | - LOOKAHEAD(2) statement = ParenthesedUpdate() - | - LOOKAHEAD(2) statement = ParenthesedDelete() - ) - { - withItem = new WithItem(statement, new Alias(name, false)) - .withRecursive(recursive, usingNot, materialized) - .withWithItemList(selectItems); - } - ) - ) - [ withSearchClause = WithSearchClause() { withItem.setSearchClause(withSearchClause); } ] - { - return withItem; - } + ":" endExpression = Expression() + { + RangeExpression result = new RangeExpression(startExpression, endExpression); + linkAST(result, firstToken(startExpression, colon)); + return result; + } } -WithSearchClause WithSearchClause() #WithSearchClause: -{ - Token orderingToken; - ExpressionList searchColumns = new ExpressionList(); - Column searchColumn = null; - String sequenceColumnName; +/** DuckDB's list comprehension {@code [expr FOR x[, i] IN list [IF condition]]}. */ +ListComprehension ListComprehension() : { + Expression expression; + List variables = new ArrayList(); + String variable; + Expression list; + Expression condition = null; +} { + "[" + expression=Expression() + variable=RelObjectName() { variables.add(variable); } + [ "," variable=RelObjectName() { variables.add(variable); } ] + list=Expression() + [ condition=Expression() ] + "]" + { return new ListComprehension(expression, variables, list, condition); } } -{ - - ( orderingToken= | orderingToken= ) - - - searchColumn = Column() { searchColumns.add(searchColumn); } - ( "," searchColumn = Column() { searchColumns.add(searchColumn); } )* - - sequenceColumnName = RelObjectName() + +ArrayConstructor ArrayConstructor(boolean arrayKeyword) : { + ExpressionList expList = new ExpressionList(); + Expression exp; + Token first = getToken(1); +} { + "[" + [ + exp=ArrayElement() { expList.add(exp); } + ( "," exp=ArrayElement() { expList.add(exp); } )* + ] + "]" { - return new WithSearchClause( - "BREADTH".equalsIgnoreCase(orderingToken.image) - ? WithSearchClause.SearchOrder.BREADTH - : WithSearchClause.SearchOrder.DEPTH, - searchColumns, sequenceColumnName); + ArrayConstructor result = new ArrayConstructor(linkElements(expList), arrayKeyword); + // after ARRAY, the caller links the constructor from that keyword + if (!arrayKeyword) { + linkAST(result, first); + } + return result; } } -WithFunctionDeclaration WithFunctionDeclaration() #WithFunctionDeclaration: +Expression ArrayElement(): { - String functionName; - List parameters = new ArrayList(); - String returnType; - Expression returnExpression; - WithFunctionParameter parameter; + Expression expression; } { - functionName = RelObjectName() - "(" - [ parameter=WithFunctionParameter() { parameters.add(parameter); } - ( "," parameter=WithFunctionParameter() { parameters.add(parameter); } )* - ] - ")" - returnType = RelObjectName() - returnExpression = Expression() - { - return new WithFunctionDeclaration(functionName, parameters, returnType, returnExpression); - } + expression=Expression() [ expression=RangeExpression(expression) ] + { return expression; } } -WithFunctionParameter WithFunctionParameter() #WithFunctionParameter: +MapExpression MapExpression() #MapExpression: { - String name; - String type = null; - String arrayType = null; + MapExpression mapExpression = new MapExpression(); + Expression key; + Expression value; } { - name = RelObjectName() - ( - LOOKAHEAD(2) "<" arrayType = RelObjectName() ">" - | - type = RelObjectName() - ) + + [ + key = MapKeyExpression() value = Expression() + { mapExpression.addEntry(key, value); } + ( + "," key = MapKeyExpression() value = Expression() + { mapExpression.addEntry(key, value); } + )* + ] + { - if (arrayType != null) { - type = "ARRAY<" + arrayType + ">"; - } - return new WithFunctionParameter(name, type); + linkAST(mapExpression, jjtThis); + return mapExpression; } } -List> ColumnSelectItemsList(): +Expression MapKeyExpression(): { - List> selectItemsList = null; - SelectItem selectItem = null; + Expression key; } { - selectItem=SelectItem() { selectItemsList = new ArrayList>(); selectItemsList.add(selectItem); } - ( - LOOKAHEAD(2) "," selectItem=SelectItem() - { - selectItemsList.add(selectItem); - } - )* - - { return selectItemsList; } + try { + { mapKeyDepth++; } + key = Expression() + } finally { + mapKeyDepth--; + } + { return key; } } -List> SelectItemsList(): +List> StructParameters(): { - List> selectItemsList = null; - SelectItem selectItem = null; + String parameterName = ""; + ColDataType parameterType = null; + AbstractMap.SimpleEntry parameter = null; + List> parameters = new ArrayList>(); } { - selectItem=SelectItem() { selectItemsList = new ArrayList>(); selectItemsList.add(selectItem); } + [ LOOKAHEAD(2) parameterName = RelObjectName() ] + parameterType = ColDataType() + { + parameters.add( new AbstractMap.SimpleEntry(parameterName, parameterType) ); + } + ( - LOOKAHEAD(2) "," selectItem=SelectItem() + "," { parameterName=""; } + + [ LOOKAHEAD(2) parameterName = RelObjectName() ] + parameterType = ColDataType() { - selectItemsList.add(selectItem); + parameters.add( new AbstractMap.SimpleEntry(parameterName, parameterType) ); } )* - { return selectItemsList; } -} - -FunctionAllColumns FunctionAllColumns() #FunctionAllColumns: -{ - Function function; -} -{ - "(" ( "(" )* function=Function() ")" ( ")" )* "." "*" { - return new FunctionAllColumns(function); + return parameters; } } -SelectItem SelectItem() #SelectItem: +StructType StructType() #StruckType: { - Expression expression; - Alias alias = null; + StructType.Dialect dialect = StructType.Dialect.BIG_QUERY; + Token tk1; + String keyword = ""; + List> parameters = null; + List> arguments = null; + String id = null; + Expression expression = null; + StructType type; } { - // @fixme: Oracle's SEQUENCE.nextval is parsed as COLUMN with a name part nextval - // @todo: parse a proper SEQUENCE instead of a COLUMN ( - LOOKAHEAD( 3 ) expression = ConnectByPriorOperator() + LOOKAHEAD(4) ( + tk1= { keyword = tk1.image; } + "<" parameters = StructParameters() ">" + "(" arguments = SelectItemsList() ")" + ) + | + ( + tk1= { keyword = tk1.image; } + "(" arguments = SelectItemsList() ")" + ) | - ( expression=Condition() { expression = prattExpressionRest(expression, 2); } ) + ( + { arguments= new ArrayList>(); dialect = StructType.Dialect.DUCKDB;} + ( id = RelObjectNameExt() | tk1= { id = tk1.image; } ) + expression = Expression() { arguments.add( new SelectItem( expression, id) ); } + + ( + "," + ( id = RelObjectNameExt() | tk1= { id = tk1.image; } ) + expression = Expression() { arguments.add( new SelectItem( expression, id) ); } + )* + + + ( + LOOKAHEAD(2) "::" "(" parameters = StructParameters() ")" + )* + ) ) - [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() ] { - SelectItem selectItem = new SelectItem(expression, alias); - linkAST(selectItem,jjtThis); - return selectItem; + type = new StructType(dialect, keyword, parameters, arguments); + linkAST(type,jjtThis); + return type; } } -/** - * Parses the AllColumns-Pattern '*'. - * - * If the allowAdditions is true, it parses additional Keywords. - */ -AllColumns AllColumns(boolean allowAdditions): -{ - ParenthesedExpressionList exceptColumns = null; - List> replaceExpressions = null; - String exceptKeyword=null; - Token tk; +JsonExpression JsonExpression(Expression expr, List> idents) : { + JsonExpression result = new JsonExpression(expr, idents); + Expression expression; + Token token=null; + ColDataType type = null; + CastExpression castExpr = null; } { - "*" - // BigData allows EXCEPT, DuckDB allows EXCLUDE - [ LOOKAHEAD(2, { allowAdditions }) ( tk= | tk= ) exceptColumns = ParenthesedColumnList() { exceptKeyword=tk.image; } ] - // BigData allows REPLACE - [ LOOKAHEAD(2, { allowAdditions }) "(" replaceExpressions = SelectItemsList() ")" ] + + // chaining JSON Expressions, e.g. + // '{"obj":{"field": "value"}}'::JSON -> 'obj'::TEXT ->> 'field'::TEXT + ( + LOOKAHEAD(2, {!interrupted} ) ( + LOOKAHEAD(2) ( + "::" type=ColDataType() + { + castExpr = new CastExpression(); + castExpr.setUseCastKeyword(false); + castExpr.setLeftExpression(result); + castExpr.setColDataType(type); + expr=castExpr; + } + ) + )+ + { + result = new JsonExpression(expr); + } + + ( + LOOKAHEAD(2, { !isPostgreSqlJsonAccess(getToken(1)) + && ((ternaryThenBranchDepth == 0 && mapKeyDepth == 0 && !isSliceColonAhead()) + || !":".equals(getToken(1).image)) }) ( + token="->" + | + token=":" + | + token="->>" + | + token="#>" + | + token="#>>" + ) + + ( + LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted}) expression=Expression() + | + expression=SimpleExpression() + ) + { + result.addIdent( expression, token.image ); + } + )* + )* { - return new AllColumns(exceptColumns, replaceExpressions, exceptKeyword); + result.setExpression(expr); + return result; } } -/** - * Parses the AllTableColumns-Pattern 'table.*' - * - * If the allowAdditions is true, it parses additional Keywords. - */ -AllTableColumns AllTableColumns(boolean allowAdditions): +JsonKeyValuePair JsonKeyValuePair(boolean isFirstEntry) : { + Token keyToken; + + boolean usingKeyKeyword = false; + boolean usingFormatJason = false; + String encoding = null; + boolean isWildcard = false; + + Object key = null; + Expression expression = null; + JsonFunctionExpression functionExpression; + + JsonKeyValuePairSeparator kvSeparator = JsonKeyValuePairSeparator.NOT_USED; +} { - Table table = null; - AllColumns allColumns; + ( + // lookahead because key is a valid column name + LOOKAHEAD(2) ( + { usingKeyKeyword = true; } + ( + keyToken = { key = keyToken.image; } | + key = Column() + ) + ) + | + LOOKAHEAD(16) key = AllTableColumns(false) { isWildcard = true; } + | + LOOKAHEAD(2) key = AllColumns(false) { isWildcard = true; } + | + LOOKAHEAD(2) key = Column() + | + LOOKAHEAD({getAsBoolean(Feature.allowExpressionAsJsonObjectKey)}) key = Expression() + | + keyToken = { key = keyToken.image; } + ) + + // Optional Separator + Value - Is not allowed with * or t1.* + [ LOOKAHEAD(1, { !isWildcard } ) + ( + { kvSeparator = JsonKeyValuePairSeparator.VALUE; } + | + { kvSeparator = JsonKeyValuePairSeparator.COLON; } + | + LOOKAHEAD({getAsBoolean(Feature.allowCommaAsKeyValueSeparator)}) { kvSeparator = JsonKeyValuePairSeparator.COMMA; } + ) + expression = Expression() + ] + + // Optional: FORMAT JSON [ ENCODING ... ] - Is not allowed with * or t1.* + [ + LOOKAHEAD(1, { !isWildcard } ) { usingFormatJason = true; } + [ encoding = JsonEncoding() ] + ] + { + final JsonKeyValuePair keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, kvSeparator ); + keyValuePair.setUsingFormatJson( usingFormatJason ); + keyValuePair.setEncoding(encoding); + return keyValuePair; + } +} + +JsonFunction JsonObjectBody() : { + JsonFunction result = new JsonFunction(JsonFunctionType.OBJECT); + + JsonKeyValuePair keyValuePair; + ColDataType dataType; + String encoding; } { - table=Table() "." allColumns=AllColumns(allowAdditions) + ( "(" + ( + // First Element + LOOKAHEAD(2, { !isJsonConstructorClauseAhead() }) + keyValuePair = JsonKeyValuePair(true) { result.add(keyValuePair);} + + // Next Elements + ( + keyValuePair = JsonKeyValuePair(false) { result.add(keyValuePair); } + )* + )? + [ + ( { result.setOnNullType( JsonAggregateOnNullType.NULL ); } ) + | + ( { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } ) + ] + [ { result.setStrict(true); } ] + [ + ( { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); } ) + | + ( { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); } ) + ] + [ + dataType = ColDataType() { result.setReturningType(dataType); } + [ + { result.setReturningFormatJson(true); } + [ encoding = JsonEncoding() { result.setReturningEncoding(encoding); } ] + ] + ] + ")" ) { - return new AllTableColumns(table, allColumns); + return result; } - } -Alias Alias(): -{ String name = ""; - Token token = null; - boolean useAs = false; - Alias alias; - String colname; - ColDataType colDataType = null; +JsonFunction JsonArrayBody() : { + JsonFunction result = new JsonFunction(JsonFunctionType.ARRAY); + + Expression expression = null; + JsonFunctionExpression functionExpression; + ColDataType dataType; + String encoding; } { - ( - LOOKAHEAD(3) ( - // Aliases with AS and Columns, but optional identifier: - // SELECT fun(x) AS (a,b,c) - // SELECT fun(x) AS T(a,b,c) - - [ LOOKAHEAD(2) name=RelObjectName() ] - { alias = new Alias(name, true ); } + ( "(" + ( + LOOKAHEAD(2) ( + { result.setOnNullType( JsonAggregateOnNullType.NULL ); } + ) + | + LOOKAHEAD(1, { !isJsonConstructorClauseAhead() }) + expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } - "(" { List list = new ArrayList(); } - colname = RelObjectName() [ colDataType = ColDataType() ] { list.add(new Alias.AliasColumn(colname, colDataType)); } + [ + LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } + [ LOOKAHEAD(2) encoding = JsonEncoding() { functionExpression.setEncoding(encoding); } ] + ] ( - "," { colDataType=null; } colname = RelObjectName() [ colDataType = ColDataType()] { list.add(new Alias.AliasColumn(colname, colDataType)); } + "," + expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } + [ + LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } + [ LOOKAHEAD(2) encoding = JsonEncoding() { functionExpression.setEncoding(encoding); } ] + ] )* - ")" { alias.setAliasColumns(list); } - - ) - | - ( - // Aliases with identifier but optional AS and Columns: - // SELECT fun(x) AS T - // SELECT fun(x) T - // SELECT fun(x) T(a,b,c) - - [ { useAs = true; } ] - ( name=RelObjectName() | token= { name=token.image; } ) - { alias = new Alias(name,useAs); } - - [ LOOKAHEAD(2) "(" { List list = new ArrayList(); } - colname = RelObjectName() [ colDataType = ColDataType() ] { list.add(new Alias.AliasColumn(colname, colDataType)); } - ("," { colDataType=null; } colname = RelObjectName() [ colDataType = ColDataType()] { list.add(new Alias.AliasColumn(colname, colDataType)); } )* - ")" { alias.setAliasColumns(list); } ] + )* - ) - ) - { return alias; } + [ + { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } + ] + [ + dataType = ColDataType() { result.setReturningType(dataType); } + [ + { result.setReturningFormatJson(true); } + [ encoding = JsonEncoding() { result.setReturningEncoding(encoding); } ] + ] + ] + ")" ) + { + return result; + } } -void SQLServerHint(SQLServerHints hints) : { - String str; +void JsonKeyword(String expectedKeyword) : { + Token token; } { - "(" str = RelObjectName() ")" { hints.setIndexName(str); } - | - { hints.withNoLock(); } + token = + { + if (!token.image.equalsIgnoreCase(expectedKeyword)) { + throw new ParseException( + "Expected keyword " + expectedKeyword + " but found " + token.image); + } + } } -SQLServerHints SQLServerHints() : { - SQLServerHints hints = new SQLServerHints(); +String JsonEncoding() : { + Token token; } { - "(" - SQLServerHint(hints) ("," SQLServerHint(hints) )* - ")" - { return hints; } + token = + { + if (token.image.equalsIgnoreCase("UTF8")) { + return "UTF8"; + } else if (token.image.equalsIgnoreCase("UTF16")) { + return "UTF16"; + } else if (token.image.equalsIgnoreCase("UTF32")) { + return "UTF32"; + } + throw new ParseException( + "Expected ENCODING value UTF8, UTF16 or UTF32 but found " + token.image); + } } -MySQLIndexHint MySQLIndexHint(): -{ - Token actionToken = null; - Token indexToken = null; - String indexName = null; - List indexNameList = new ArrayList(); +JsonFunctionExpression JsonValueOrQueryInputExpression() : { + Expression expression; + JsonFunctionExpression functionExpression; + String encoding; } { - ( - actionToken = - | actionToken = - | actionToken = - | actionToken = - ) - - ( - indexToken = - | indexToken = - ) - - "(" - indexName = RelObjectName() { indexNameList.add(indexName); } - ("," indexName= RelObjectName() { indexNameList.add(indexName); })* - ")" - { - return new MySQLIndexHint(actionToken.image, indexToken.image, indexNameList); - } + expression = Expression() { functionExpression = new JsonFunctionExpression(expression); } + [ + { functionExpression.setUsingFormatJson(true); } + [ encoding = JsonEncoding() { functionExpression.setEncoding(encoding); } ] + ] + { + return functionExpression; + } } -SelectItem FunctionItem(): -{ - Alias alias = null; - Function function; +JsonFunction.JsonOnResponseBehavior JsonValueOnResponseBehavior() : { + JsonFunction.JsonOnResponseBehavior behavior; + Expression expression; } { - function=Function() - [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() ] - { return new SelectItem(function, alias); } + ( + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.ERROR); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.NULL); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY); + } + | + expression = Expression() + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.DEFAULT, expression); + } + ) + { + return behavior; + } } -ExpressionList PivotForColumns(): -{ - ExpressionList columns; - Column column; +JsonFunction.JsonOnResponseBehavior JsonQueryOnResponseBehavior() : { + JsonFunction.JsonOnResponseBehavior behavior = null; + Token token; } { ( - columns = ParenthesedColumnList() + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.ERROR); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.NULL); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.TRUE); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.FALSE); + } | - column = Column() { columns = new ExpressionList(column); } + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY); + } + [ + ( + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY); + } + | + JsonKeyword("OBJECT") + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY_OBJECT); + } + ) + ] ) - { return columns; } + { + return behavior; + } } -List> PivotFunctionItems(): -{ - List> functionItems = new ArrayList>(); - SelectItem item; +JsonFunction.JsonOnResponseBehavior JsonExistsOnResponseBehavior() : { + JsonFunction.JsonOnResponseBehavior behavior = null; } { - item = FunctionItem() {functionItems.add(item);} - ( "," item = FunctionItem() {functionItems.add(item);} )* - { return functionItems; } + ( + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.TRUE); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.FALSE); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.UNKNOWN); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.ERROR); + } + ) + { + return behavior; + } } -SelectItem> ExpressionListItem(): -{ - ExpressionList expressionList; - Alias alias = null; +JsonFunction JsonExistsBody() : { + JsonFunction result = new JsonFunction(JsonFunctionType.EXISTS); + JsonFunctionExpression inputExpression; + Expression expression; + JsonFunction.JsonOnResponseBehavior behavior; } { - expressionList=ParenthesedExpressionList() - [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() ] - { return new SelectItem>(expressionList, alias); } + "(" + inputExpression = JsonValueOrQueryInputExpression() { result.setInputExpression(inputExpression); } + "," + expression = Expression() { result.setJsonPathExpression(expression); } + + [ + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) + JsonKeyword("PASSING") + expression = Expression() { result.addPassingExpression(expression); } + ( LOOKAHEAD(2) "," expression = Expression() { result.addPassingExpression(expression); } )* + ] + + [ + LOOKAHEAD( JsonExistsOnResponseBehavior() ) + behavior = JsonExistsOnResponseBehavior() + + { result.setOnErrorBehavior(behavior); } + ] + ")" + { + return result; + } } -List>> PivotMultiInItems(): -{ - List>> retval = new ArrayList>>(); - SelectItem> item; +JsonFunction JsonValueBody() : { + JsonFunction result = new JsonFunction(JsonFunctionType.VALUE); + JsonFunctionExpression inputExpression; + Expression expression; + ColDataType dataType; + JsonFunction.JsonOnResponseBehavior behavior; } { - item = ExpressionListItem() {retval.add(item);} - ("," item = ExpressionListItem() {retval.add(item);} )* - { return retval; } + "(" + inputExpression = JsonValueOrQueryInputExpression() { result.setInputExpression(inputExpression); } + "," + expression = Expression() { result.setJsonPathExpression(expression); } + + [ + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) + JsonKeyword("PASSING") + expression = Expression() { result.addPassingExpression(expression); } + ( LOOKAHEAD(2) "," expression = Expression() { result.addPassingExpression(expression); } )* + ] + + [ dataType = ColDataType() { result.setReturningType(dataType); } ] + + [ + LOOKAHEAD( JsonValueOnResponseBehavior() ) + behavior = JsonValueOnResponseBehavior() + + { result.setOnEmptyBehavior(behavior); } + ] + + [ + LOOKAHEAD( JsonValueOnResponseBehavior() ) + behavior = JsonValueOnResponseBehavior() + + { result.setOnErrorBehavior(behavior); } + ] + ")" + { + return result; + } } -Pivot Pivot(): -{ - Pivot retval = new Pivot(); - List> functionItems; - ExpressionList forColumns; - List> singleInItems = null; - List>> multiInItems = null; - Alias alias = null; +JsonFunction JsonQueryBody() : { + JsonFunction result = new JsonFunction(JsonFunctionType.QUERY); + JsonFunctionExpression inputExpression; + Expression expression; + ColDataType dataType; + JsonFunction.JsonOnResponseBehavior behavior; + Token token; + String encoding; + ColDataType additionalReturningType; + boolean additionalReturningFormatJson; + String additionalReturningEncoding; + JsonFunction.JsonWrapperType additionalWrapperType; + JsonFunction.JsonWrapperMode additionalWrapperMode; + boolean additionalWrapperArray; + JsonFunction.JsonQuotesType additionalQuotesType; + boolean additionalQuotesOnScalarString; + JsonFunction.JsonOnResponseBehavior additionalOnEmptyBehavior; + JsonFunction.JsonOnResponseBehavior additionalOnErrorBehavior; + StringBuilder additionalBuilder; + boolean hasPassingClause = false; } { - "(" functionItems = PivotFunctionItems() - forColumns = PivotForColumns() - "(" - (LOOKAHEAD(3) singleInItems = SelectItemsList() - | multiInItems = PivotMultiInItems() ) - ")" + "(" + inputExpression = JsonValueOrQueryInputExpression() { result.setInputExpression(inputExpression); } + "," + expression = Expression() { result.setJsonPathExpression(expression); } + + [ + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) + JsonKeyword("PASSING") { hasPassingClause = true; } + expression = Expression() { result.addPassingExpression(expression); } + ( LOOKAHEAD(2) "," expression = Expression() { result.addPassingExpression(expression); } )* + ] + + [ + dataType = ColDataType() { result.setReturningType(dataType); } + [ + { result.setReturningFormatJson(true); } + [ encoding = JsonEncoding() { result.setReturningEncoding(encoding); } ] + ] + ] + + [ + ( + { result.setWrapperType(JsonFunction.JsonWrapperType.WITHOUT); } + [ { result.setWrapperArray(true); } ] + JsonKeyword("WRAPPER") + | + { result.setWrapperType(JsonFunction.JsonWrapperType.WITH); } + [ + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && (getToken(1).image.equalsIgnoreCase("CONDITIONAL") + || getToken(1).image.equalsIgnoreCase("UNCONDITIONAL")) + }) + token = + { + if (token.image.equalsIgnoreCase("CONDITIONAL")) { + result.setWrapperMode(JsonFunction.JsonWrapperMode.CONDITIONAL); + } else { + result.setWrapperMode(JsonFunction.JsonWrapperMode.UNCONDITIONAL); + } + } + ] + [ { result.setWrapperArray(true); } ] + JsonKeyword("WRAPPER") + ) + ] + + [ + LOOKAHEAD({ + getToken(1).kind == K_KEEP + || (getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("OMIT")) + }) + ( + { result.setQuotesType(JsonFunction.JsonQuotesType.KEEP); } + | + JsonKeyword("OMIT") { result.setQuotesType(JsonFunction.JsonQuotesType.OMIT); } + ) + JsonKeyword("QUOTES") + [ + JsonKeyword("SCALAR") + { result.setQuotesOnScalarString(true); } + ] + ] + + [ + LOOKAHEAD( JsonQueryOnResponseBehavior() ) + behavior = JsonQueryOnResponseBehavior() + + { result.setOnEmptyBehavior(behavior); } + ] + + [ + LOOKAHEAD( JsonQueryOnResponseBehavior() ) + behavior = JsonQueryOnResponseBehavior() + + { result.setOnErrorBehavior(behavior); } + ] + + ( + LOOKAHEAD(2, { !hasPassingClause }) + "," + { + additionalReturningType = null; + additionalReturningFormatJson = false; + additionalReturningEncoding = null; + additionalWrapperType = null; + additionalWrapperMode = null; + additionalWrapperArray = false; + additionalQuotesType = null; + additionalQuotesOnScalarString = false; + additionalOnEmptyBehavior = null; + additionalOnErrorBehavior = null; + } + expression = Expression() + [ + additionalReturningType = ColDataType() + [ + { additionalReturningFormatJson = true; } + [ additionalReturningEncoding = JsonEncoding() ] + ] + ] + [ + ( + + { additionalWrapperType = JsonFunction.JsonWrapperType.WITHOUT; } + [ { additionalWrapperArray = true; } ] + JsonKeyword("WRAPPER") + | + + { additionalWrapperType = JsonFunction.JsonWrapperType.WITH; } + [ + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && (getToken(1).image.equalsIgnoreCase("CONDITIONAL") + || getToken(1).image.equalsIgnoreCase("UNCONDITIONAL")) + }) + token = + { + if (token.image.equalsIgnoreCase("CONDITIONAL")) { + additionalWrapperMode = JsonFunction.JsonWrapperMode.CONDITIONAL; + } else { + additionalWrapperMode = JsonFunction.JsonWrapperMode.UNCONDITIONAL; + } + } + ] + [ { additionalWrapperArray = true; } ] + JsonKeyword("WRAPPER") + ) + ] + [ + LOOKAHEAD({ + getToken(1).kind == K_KEEP + || (getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("OMIT")) + }) + ( + { additionalQuotesType = JsonFunction.JsonQuotesType.KEEP; } + | + JsonKeyword("OMIT") { additionalQuotesType = JsonFunction.JsonQuotesType.OMIT; } + ) + JsonKeyword("QUOTES") + [ + JsonKeyword("SCALAR") { additionalQuotesOnScalarString = true; } + ] + ] + [ + LOOKAHEAD( JsonQueryOnResponseBehavior() ) + additionalOnEmptyBehavior = JsonQueryOnResponseBehavior() + + ] + [ + LOOKAHEAD( JsonQueryOnResponseBehavior() ) + additionalOnErrorBehavior = JsonQueryOnResponseBehavior() + + ] + { + additionalBuilder = new StringBuilder(); + additionalBuilder.append(expression); + if (additionalReturningType != null) { + additionalBuilder.append(" RETURNING ").append(additionalReturningType); + if (additionalReturningFormatJson) { + additionalBuilder.append(" FORMAT JSON"); + if (additionalReturningEncoding != null) { + additionalBuilder.append(" ENCODING ").append(additionalReturningEncoding); + } + } + } + if (additionalWrapperType != null) { + additionalBuilder.append(" ").append(additionalWrapperType); + if (additionalWrapperMode != null) { + additionalBuilder.append(" ").append(additionalWrapperMode); + } + if (additionalWrapperArray) { + additionalBuilder.append(" ARRAY"); + } + additionalBuilder.append(" WRAPPER"); + } + if (additionalQuotesType != null) { + additionalBuilder.append(" ").append(additionalQuotesType).append(" QUOTES"); + if (additionalQuotesOnScalarString) { + additionalBuilder.append(" ON SCALAR STRING"); + } + } + if (additionalOnEmptyBehavior != null) { + additionalBuilder.append(" ").append(additionalOnEmptyBehavior).append(" ON EMPTY"); + } + if (additionalOnErrorBehavior != null) { + additionalBuilder.append(" ").append(additionalOnErrorBehavior).append(" ON ERROR"); + } + result.addAdditionalQueryPathArgument(additionalBuilder.toString()); + } + )* ")" - [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] { - retval.setFunctionItems(functionItems); - retval.setForColumns(forColumns); - retval.setSingleInItems(singleInItems); - retval.setMultiInItems(multiInItems); - retval.setAlias(alias); - return retval; + return result; } } -PivotXml PivotXml(): -{ - PivotXml retval = new PivotXml(); - List> functionItems; - ExpressionList forColumns; - List> singleInItems = null; - List>> multiInItems = null; - Select inSelect = null; +JsonFunction JsonFunction() : { + JsonFunction result; } { - "(" functionItems = PivotFunctionItems() - forColumns = PivotForColumns() - "(" ( - LOOKAHEAD(2) { retval.setInAny(true); } | - LOOKAHEAD(1) inSelect = Select() | - LOOKAHEAD(2) singleInItems =SelectItemsList() | - multiInItems = PivotMultiInItems() + ( result = JsonObjectBody() ) + | + ( result = JsonArrayBody() ) + | + ( + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_VALUE") + }) + JsonKeyword("JSON_VALUE") + result = JsonValueBody() + ) + | + ( + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_QUERY") + }) + JsonKeyword("JSON_QUERY") + result = JsonQueryBody() + ) + | + ( + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_EXISTS") + }) + JsonKeyword("JSON_EXISTS") + result = JsonExistsBody() + ) ) - ")" - ")" - { - retval.setFunctionItems(functionItems); - retval.setForColumns(forColumns); - retval.setSingleInItems(singleInItems); - retval.setMultiInItems(multiInItems); - retval.setInSelect(inSelect); - return retval; - } -} - -UnPivot UnPivot(): -{ - UnPivot retval = new UnPivot(); - ExpressionList unpivotClause; - ExpressionList unpivotForClause; - List> unpivotInClause; - Alias alias = null; -} -{ - - [ ( { retval.setIncludeNulls(true); } - | { retval.setIncludeNulls(false); } ) ] - "(" unpivotClause = PivotForColumns() - unpivotForClause = PivotForColumns() - "(" - unpivotInClause = SelectItemsList() - ")" - ")" - [ LOOKAHEAD({ isAliasAhead() }) alias = Alias() ] - { - retval.setUnPivotClause(unpivotClause); - retval.setUnPivotForClause(unpivotForClause); - retval.setUnPivotInClause(unpivotInClause); - retval.setAlias(alias); - return retval; - } -} - -List
IntoClause(): -{ - List
tables = new ArrayList
(); - Table table; -} -{ - table=Table() { tables.add(table); } ( LOOKAHEAD(2) "," table=Table() { tables.add(table); } )* { - return tables; + return result; } } -MySqlSelectIntoClause MySqlSelectIntoClause(MySqlSelectIntoClause.Position position): -{ - MySqlSelectIntoClause intoClause = new MySqlSelectIntoClause().withPosition(position); +JsonAggregateFunction JsonAggregateFunction() : { + JsonAggregateFunction result = new JsonAggregateFunction(); Token token; + Object key; + Expression expression; + List expressionOrderByList = null; + + Expression filter; + ExpressionList expressionList = null; + List olist = null; + WindowElement windowElement = null; + Token partitionFirst = null; + Token partitionLast = null; } { - ( - { intoClause.setType(MySqlSelectIntoClause.Type.OUTFILE); } - token= { intoClause.setFileName(new StringValue(token.image)); } - MySqlSelectIntoOutfileTail(intoClause) - | - { intoClause.setType(MySqlSelectIntoClause.Type.DUMPFILE); } - token= { intoClause.setFileName(new StringValue(token.image)); } + ( + ( + "(" { result.setType( JsonFunctionType.OBJECT ); } + ( + LOOKAHEAD(2) ( + "KEY" { result.setUsingKeyKeyword( true ); } + ( + ( token = | token = | token = | token = | token = ) + { + key = token.image; + } + | + key = Column() + ) + ) + | + ( token = | token = | token = | token = | token = ) + { + key = token.image; + } + | + key = Column() + ) + { + result.setKey( key ); + } + + ( ":" | "," { result.setType( JsonFunctionType.MYSQL_OBJECT ); } | "VALUE" {result.setUsingValueKeyword( true ); } ) + expression = Expression() { result.setValue( expression ); } + + [ { result.setUsingFormatJson( true ); } ] + + [ + LOOKAHEAD(2) ( + { result.setOnNullType( JsonAggregateOnNullType.NULL ); } + ) + | + ( + { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } + ) + ] + + [ + ( + { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); } + ) + | + ( + { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); } + ) + ] + ")" + ) + | + ( + + "(" { result.setType( JsonFunctionType.ARRAY ); } + expression=Expression() { result.setExpression( expression ); } + [ { result.setUsingFormatJson( true ); } ] + [ expressionOrderByList = OrderByElements() { result.setExpressionOrderByElements( expressionOrderByList ); } ] + + [ + LOOKAHEAD(2) ( + { result.setOnNullType( JsonAggregateOnNullType.NULL ); } + ) + | + ( + { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } + ) + ] + + ")" + ) + ) + + // -- Filter + [ LOOKAHEAD(2) "(" { result.setAnalyticType(AnalyticType.FILTER_ONLY); } filter = Expression() { result.setFilterExpression( filter ); } ")" ] + + // -- OVER + [ LOOKAHEAD(2) + {result.setAnalyticType(AnalyticType.OVER);} + "(" + [partitionFirst= + expressionList=ComplexExpressionList() + { partitionLast = getToken(0); } + ] + [olist=OrderByElements() ] + [windowElement = WindowElement() ] + { + result.setPartitionExpressionList(expressionList, false); + if (partitionFirst != null) { + linkAST(result.getPartitionExpressionList(), partitionFirst, partitionLast); + } + result.setOrderByElements(olist); + result.setWindowElement(windowElement); + } + ")" + ] ) + { - return intoClause; + return result; } } -void MySqlSelectIntoOutfileTail(MySqlSelectIntoClause intoClause): -{ - Token token; +IntervalExpression IntervalExpression() : { + IntervalExpression interval = new IntervalExpression(); + Token token = null; + Expression expr = null; + boolean signed = false; + IntervalQualifier qualifier = null; } { - ( - LOOKAHEAD( ) - - (token= | token=) - { intoClause.setCharacterSet(token.image); } - ( - LOOKAHEAD(( | )) - MySqlSelectIntoFieldsClause(intoClause) - [ LOOKAHEAD() MySqlSelectIntoLinesClause(intoClause) ] - | - LOOKAHEAD() - MySqlSelectIntoLinesClause(intoClause) - | - { } - ) - | - LOOKAHEAD(( | )) - MySqlSelectIntoFieldsClause(intoClause) - [ LOOKAHEAD() MySqlSelectIntoLinesClause(intoClause) ] - | - LOOKAHEAD() - MySqlSelectIntoLinesClause(intoClause) - | - { } - ) -} -void MySqlSelectIntoFieldsClause(MySqlSelectIntoClause intoClause): -{ - Token token; -} -{ - ( - { intoClause.setFieldsKeyword(MySqlSelectIntoClause.FieldsKeyword.FIELDS); } - | { intoClause.setFieldsKeyword(MySqlSelectIntoClause.FieldsKeyword.COLUMNS); } - ) + ( - LOOKAHEAD( ) - token= - { intoClause.setFieldsTerminatedBy(new StringValue(token.image)); } - ( - LOOKAHEAD(( | )) - [ { intoClause.setFieldsOptionallyEnclosed(true); } ] - token= - { intoClause.setFieldsEnclosedBy(new StringValue(token.image)); } - [ LOOKAHEAD( ) - token= - { intoClause.setFieldsEscapedBy(new StringValue(token.image)); } - ] - | - LOOKAHEAD( ) - token= - { intoClause.setFieldsEscapedBy(new StringValue(token.image)); } - | - { } - ) - | - LOOKAHEAD(( | )) - [ { intoClause.setFieldsOptionallyEnclosed(true); } ] - token= - { intoClause.setFieldsEnclosedBy(new StringValue(token.image)); } - [ LOOKAHEAD( ) - token= - { intoClause.setFieldsEscapedBy(new StringValue(token.image)); } - ] - | - LOOKAHEAD( ) - token= - { intoClause.setFieldsEscapedBy(new StringValue(token.image)); } - | - { } - ) -} -void MySqlSelectIntoLinesClause(MySqlSelectIntoClause intoClause): -{ - Token token; -} -{ - - ( - LOOKAHEAD( ) - token= - { intoClause.setLinesStartingBy(new StringValue(token.image)); } - [ LOOKAHEAD( ) - token= - { intoClause.setLinesTerminatedBy(new StringValue(token.image)); } - ] + LOOKAHEAD(3) ( + [ "-" {signed=true;}] + (token= | token=) + ) | - LOOKAHEAD( ) - token= - { intoClause.setLinesTerminatedBy(new StringValue(token.image)); } + LOOKAHEAD(2) token= | - { } + expr = Expression() ) + { + if (expr != null) { + if (signed) expr = new SignedExpression('-', expr); + interval.setExpression(expr); + } else { + interval.setParameter((signed?"-":"") + token.image); + } + } + [ + // The entry decision for this optional block has to be stated explicitly: + // also starts whatever follows IntervalExpression() at its call sites (an alias, for + // instance), so one token of lookahead cannot decide whether to enter. + LOOKAHEAD({ getToken(1).kind == K_DATE_LITERAL || getToken(1).kind == S_IDENTIFIER }) + ( + // Standard qualifier (field [TO field] [(precision)]) such as DAY or DAY TO SECOND. + LOOKAHEAD({ getToken(1).kind == K_DATE_LITERAL }) qualifier = IntervalQualifier() { interval.setIntervalQualifier(qualifier); } + | + // Non-standard single identifier field (e.g. MySQL `INTERVAL 1 foo`), kept for + // backwards compatibility. + token = { interval.setIntervalType(token.image); } + ) + ] + { + return interval; + } } -FromItem ParenthesedFromItem(): -{ - ParenthesedFromItem ParenthesedFromItem = new ParenthesedFromItem(); - FromItem fromItem; - List joins = null; +IntervalExpression IntervalExpressionWithoutInterval(Expression expr) : { + IntervalExpression interval; + IntervalQualifier qualifier = null; + boolean signed = false; } { - "(" - fromItem = FromItem() - [ joins=JoinsList() ] - ")" - { - return ParenthesedFromItem.withFromItem(fromItem).withJoins(joins); + interval = new IntervalExpression(false); + interval.setExpression(expr); + } + qualifier = IntervalQualifier() + { + interval.setIntervalQualifier(qualifier); + return interval; } } -FromItem FromItem() #FromItem: -{ - FromItem fromItem = null; - FromItem fromItem2 = null; - SampleClause sampleClause; - Pivot pivot = null; - UnPivot unpivot = null; - Alias alias = null; - MySQLIndexHint indexHint = null; - SQLServerHints sqlServerHints = null; - Select select; - - String timeTravelStr = null; +/** + * Parses an SQL interval qualifier, e.g. {@code DAY}, {@code DAY TO SECOND}, + * {@code DAY(9) TO SECOND} or {@code SECOND(2, 4)}, as defined by the SQL standard. + * Shared by {@link IntervalExpression} (DML interval literals) and by {@code ColDataType} + * (DDL {@code INTERVAL} column / cast target types). + */ +IntervalQualifier IntervalQualifier() : { + Token field = null; + Token toField = null; + Token leadingPrecision = null; + Token fractionalPrecision = null; + Token trailingFractionalPrecision = null; } { - ( - LOOKAHEAD(3, { !getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) fromItem = Values() - | - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("JSON_TABLE") - && getToken(2).kind == OPENING_BRACKET - }) fromItem=TableFunction() - | - LOOKAHEAD({ - getToken(1).kind == K_LATERAL - && getToken(2).kind == S_IDENTIFIER - && getToken(2).image.equalsIgnoreCase("JSON_TABLE") - && getToken(3).kind == OPENING_BRACKET - }) fromItem=TableFunction() - | - LOOKAHEAD({ - (getToken(1).kind == K_ROWS - && getToken(2).kind == K_FROM - && getToken(3).kind == OPENING_BRACKET) - || (isFunctionAhead() && getToken(1).kind != K_LATERAL) - || (getToken(1).kind == K_LATERAL && getToken(2).kind != OPENING_BRACKET) }) - fromItem=TableFunction() - | - LOOKAHEAD(3) fromItem=Table() - | - LOOKAHEAD({ isNestedSetOperationAhead() }) ( - fromItem=ParenthesedSelect() - [ LOOKAHEAD(2) pivot=Pivot() { fromItem.setPivot(pivot); } ] - [ LOOKAHEAD(2) unpivot=UnPivot() { fromItem.setUnPivot(unpivot); } ] - ) - | - LOOKAHEAD({ isParenthesedFromItemAhead() }) fromItem = ParenthesedFromItem() - | - LOOKAHEAD(3, { !getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) ( - fromItem=ParenthesedSelect() - [ LOOKAHEAD(2) pivot=Pivot() { fromItem.setPivot(pivot); } ] - [ LOOKAHEAD(2) unpivot=UnPivot() { fromItem.setUnPivot(unpivot); } ] - ) - | - fromItem=LateralSubSelect() - | - LOOKAHEAD(2, { Dialect.EXASOL.name().equals(getAsString(Feature.dialect)) }) fromItem=SubImport() { fromItem = new ParenthesedFromItem(fromItem); } - | - LOOKAHEAD({ getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) fromItem=Select() - ) - - [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { fromItem.setAlias(alias); } ] + field = [ - LOOKAHEAD(2, {fromItem instanceof Table }) timeTravelStr = TimeTravelAfterAlias() - { ((Table) fromItem).setTimeTravelStrAfterAlias(timeTravelStr); } + LOOKAHEAD(2) "(" leadingPrecision = + [ "," fractionalPrecision = ] + ")" ] - [ LOOKAHEAD(2) sampleClause = SampleClause() { fromItem.setSampleClause(sampleClause); } ] - [ LOOKAHEAD(2) unpivot=UnPivot() { fromItem.setUnPivot(unpivot); } ] - [ LOOKAHEAD(2) ( LOOKAHEAD(2) pivot=PivotXml() | pivot=Pivot() ) { fromItem.setPivot(pivot); } ] [ - LOOKAHEAD(2) - ( - indexHint = MySQLIndexHint() { - if (fromItem instanceof Table) - ((Table) fromItem).setHint(indexHint); - } - | - sqlServerHints = SQLServerHints() { - if (fromItem instanceof Table) - ((Table) fromItem).setSqlServerHints(sqlServerHints); - } - ) + LOOKAHEAD(2) toField = + [ LOOKAHEAD(2) "(" trailingFractionalPrecision = ")" ] ] { - linkAST(fromItem,jjtThis); - return fromItem; + // SECOND(2, 4) TO SECOND(3) specifies fractional seconds precision for both the leading + // and the trailing field. Reject such contradictory input instead of silently dropping one + // of the values. The rejection is placed last so the production does not complete normally + // and JavaCC does not append an unreachable "missing return" guard. + if (fractionalPrecision == null || trailingFractionalPrecision == null) { + return new IntervalQualifier( + field != null ? field.image : null, + leadingPrecision != null ? Integer.valueOf(leadingPrecision.image) : null, + toField != null ? toField.image : null, + trailingFractionalPrecision != null + ? Integer.valueOf(trailingFractionalPrecision.image) + : (fractionalPrecision != null + ? Integer.valueOf(fractionalPrecision.image) : null)); + } + throw new ParseException( + "Invalid interval qualifier: fractional seconds precision is specified for both" + + " the leading field and the trailing field"); } } -List JoinsList(): -{ - List joinsList = new ArrayList(); - Join join = null; +KeepExpression KeepExpression() : { + KeepExpression keep = new KeepExpression(); + Token token; + List list; + Token first = getToken(1); } { - ( LOOKAHEAD(2) join=JoinerExpression() { joinsList.add(join); } )+ + "(" token = ( { keep.setFirst(true); } | { keep.setFirst(false); }) + list = OrderByElements() ")" + { - return joinsList; + keep.setName(token.image); + keep.setOrderByElements(list); + linkAST(keep, first); + return keep; } } -JoinHint JoinHint(): -{ - Token token; -} -{ + +void windowFun(AnalyticExpression retval):{ + ExpressionList expressionList = null; + Token partitionFirst = null; + Token partitionLast = null; + String windowName = null; + WindowDefinition winDef; +} { ( - token = + {retval.setType(AnalyticType.OVER);} | - token = + {retval.setType(AnalyticType.WITHIN_GROUP);} + ) + + ( + windowName = RelObjectName() { retval.setWindowName(windowName); } | - token = + winDef = windowDefinition() + { + if (winDef.getBaseWindowName() != null && retval.getType() != AnalyticType.OVER) { + throw new ParseException("Only an OVER window may extend a named window"); + } + retval.setWindowDefinition(winDef); + } + + [ + LOOKAHEAD(2) "(" + [ partitionFirst= + expressionList=ComplexExpressionList() + { partitionLast = getToken(0); } + ] + ")" { + winDef.setPartitionExpressionList(expressionList, false); + if (partitionFirst != null) { + linkAST(winDef.getPartitionBy(), partitionFirst, partitionLast); + } + retval.setType(AnalyticType.WITHIN_GROUP_OVER); + } + ] + ) +} + +WindowDefinition windowDefinition() : { + ExpressionList expressionList = null; + List olist = null; + WindowElement windowElement = null; + Token partitionFirst = null; + Token partitionLast = null; + WindowDefinition winDef = new WindowDefinition(); + String baseWindowName; +} { + "(" + // DuckDB: (w ORDER BY b) extends the named window w. A choice, not an optional, so that + // lookahead scans through a window specification keep the check + ( + LOOKAHEAD({ isBaseWindowNameAhead() }) baseWindowName=RelObjectName() + { winDef.setBaseWindowName(baseWindowName); } | - token = + {} ) + [partitionFirst= + expressionList=ComplexExpressionList() + { partitionLast = getToken(0); } + ] + [olist=OrderByElements() ] + [windowElement = WindowElement() ] { - return new JoinHint(token.image); + winDef.setPartitionExpressionList(expressionList, false); + if (partitionFirst != null) { + linkAST(winDef.getPartitionBy(), partitionFirst, partitionLast); + } + winDef.setOrderByElements(olist); + winDef.setWindowElement(windowElement); } + ")" + { return winDef; } } -Join JoinerExpression() #JoinerExpression: +AnalyticExpression AnalyticExpression(Function function) : { - Join join = new Join(); - FromItem right = null; - Expression onExpression = null; - Column tableColumn; - List columns = null; - KSQLJoinWindow joinWindow = null; - JoinHint joinHint = null; + AnalyticExpression retval = new AnalyticExpression(function); + Expression filter = null; } { - [ { join.setGlobal(true); } ] - [ { join.setAny(true); } | { join.setAll(true); } ] - [ { join.setNatural(true); } ] - - [ - ( - { join.setLeft(true); } [ { join.setSemi(true); } | { join.setOuter(true); } | { join.setAny(true); } | { join.setAll(true); } ] - | - ( - { join.setRight(true); } - | - { join.setFull(true); } - ) [ { join.setOuter(true); } | { join.setAny(true); } | { join.setAll(true); } ] - | - { join.setInner(true); } - ) - | - { join.setCross(true); } - | - { join.setOuter(true); } - ] - ( ( - [ joinHint=JoinHint() {join.setJoinHint(joinHint); } ] - - [ { join.setFetch(true); } ] + filter=AggregateFilter(retval) { retval.setType(AnalyticType.FILTER_ONLY); } + [ LOOKAHEAD(2) windowFun(retval) ] + { + if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && (retval.getType() == AnalyticType.WITHIN_GROUP + || retval.getType() == AnalyticType.WITHIN_GROUP_OVER)) { + throw new ParseException("FILTER must follow WITHIN GROUP"); + } + } ) - | - "," { join.setSimple(true); } ( { join.setOuter(true); } )? - | - { join.setStraight(true); } - | - {join.setApply(true); } + | windowFun(retval) + [ LOOKAHEAD({ retval.getType() == AnalyticType.WITHIN_GROUP + && getToken(1).kind == K_FILTER }) filter=AggregateFilter(retval) ] ) - - right=FromItem() - - [ - LOOKAHEAD(2) ( - [ "(" joinWindow = JoinWindow() ")" {join.setJoinWindow(joinWindow);} ] - ( onExpression=Expression() { join.addOnExpression(onExpression); } - ( LOOKAHEAD(2) onExpression=Expression() { join.addOnExpression(onExpression); } )* - ) - | - ( - "(" tableColumn=Column() { columns = new ArrayList(); columns.add(tableColumn); } - ( "," tableColumn=Column() { columns.add(tableColumn); } ) * - ")" { join.setUsingColumns(columns); } - ) - ) - ] { - linkAST(join,jjtThis); - join.setFromItem(right); - return join; + retval.setFilterExpression(filter); + if (true) { return retval; } } +} +/** + * Shared FILTER predicate for ordinary, window, and ordered-set aggregates. DuckDB also accepts + * it without WHERE, FILTER (x > 1), but PostgreSQL does not. + */ +Expression AggregateFilter(AnalyticExpression analyticExpression): +{ Expression filter; } +{ + "(" + ( + + | + LOOKAHEAD({ !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) }) + { analyticExpression.setFilterWhereKeyword(false); } + ) + filter=Expression() ")" + { return filter; } } -KSQLJoinWindow JoinWindow(): +WindowElement WindowElement(): { - KSQLJoinWindow retval = new KSQLJoinWindow(); - boolean beforeAfter; - Token beforeDurationToken = null; - Token beforeTimeUnitToken = null; - Token afterDurationToken = null; - Token afterTimeUnitToken = null; + WindowElement windowElement = new WindowElement(); + WindowRange range = new WindowRange(); + WindowOffset offset = null; + WindowElement.Exclusion exclusion = null; } { - beforeDurationToken= (beforeTimeUnitToken= | beforeTimeUnitToken=) - [ "," afterDurationToken= (afterTimeUnitToken= | afterTimeUnitToken=) ] + ( { windowElement.setType(WindowElement.Type.ROWS); } + | { windowElement.setType(WindowElement.Type.RANGE); } + | { windowElement.setType(WindowElement.Type.GROUPS); }) + ( LOOKAHEAD(1) ( + { windowElement.setRange(range); } + offset = WindowOffset() { range.setStart(offset); } + offset = WindowOffset() { range.setEnd(offset); } + ) + | + offset = WindowOffset() { windowElement.setOffset(offset); } + ) + [ exclusion = FrameExclusion() { windowElement.setExclusion(exclusion); } ] + { - if (afterDurationToken == null) { - retval.setDuration(Long.parseLong(beforeDurationToken.image)); - retval.setTimeUnit(KSQLWindow.TimeUnit.from(beforeTimeUnitToken.image)); - retval.setBeforeAfterWindow(false); - return retval; - } - retval.setBeforeDuration(Long.parseLong(beforeDurationToken.image)); - retval.setBeforeTimeUnit(KSQLWindow.TimeUnit.from(beforeTimeUnitToken.image)); - retval.setAfterDuration(Long.parseLong(afterDurationToken.image)); - retval.setAfterTimeUnit(KSQLWindow.TimeUnit.from(afterTimeUnitToken.image)); - retval.setBeforeAfterWindow(true); - return retval; + return windowElement; } } -KSQLWindow KSQLWindowClause(): +WindowElement.Exclusion FrameExclusion(): { - KSQLWindow retval = null; - Token sizeDurationToken = null; - Token sizeTimeUnitToken = null; - Token advanceDurationToken = null; - Token advanceTimeUnitToken = null; + WindowElement.Exclusion exclusion = null; } { - - { - retval=new KSQLWindow(); - retval.setHoppingWindow(false); - retval.setSessionWindow(false); - retval.setTumblingWindow(false); - } + ( - "(" - sizeDurationToken= sizeTimeUnitToken= "," - advanceDurationToken= advanceTimeUnitToken= ")" - { - retval.setHoppingWindow(true); - } | - "(" sizeDurationToken= sizeTimeUnitToken= ")" - { - retval.setSessionWindow(true); - } | - "(" sizeDurationToken= sizeTimeUnitToken= ")" - { - retval.setTumblingWindow(true); - } + + { exclusion = WindowElement.Exclusion.CURRENT_ROW; } + | + + { exclusion = WindowElement.Exclusion.GROUP; } + | + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("TIES") + }) + + { exclusion = WindowElement.Exclusion.TIES; } + | + + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("OTHERS") + }) + + { exclusion = WindowElement.Exclusion.NO_OTHERS; } ) - { - retval.setSizeDuration(Long.parseLong(sizeDurationToken.image)); - retval.setSizeTimeUnit(KSQLWindow.TimeUnit.from(sizeTimeUnitToken.image)); - if (advanceDurationToken != null) { - retval.setAdvanceDuration(Long.parseLong(advanceDurationToken.image)); - retval.setAdvanceTimeUnit(KSQLWindow.TimeUnit.from(advanceTimeUnitToken.image)); - } - return retval; - } + { return exclusion; } } -Expression WhereClause(): +WindowOffset WindowOffset(): { - Expression retval = null; + WindowOffset offset = new WindowOffset(); + Expression expr = null; } { - retval=Expression() - { return retval; } + ( + LOOKAHEAD(2) ( + ( { offset.setType(WindowOffset.Type.PRECEDING); } | + { offset.setType(WindowOffset.Type.FOLLOWING); } ) + ) + | + LOOKAHEAD(2) ( { offset.setType(WindowOffset.Type.CURRENT); } ) + | + ( expr = SimpleExpression() { + offset.setType(WindowOffset.Type.EXPR); + offset.setExpression(expr); + } + ( { offset.setType(WindowOffset.Type.PRECEDING); } | { offset.setType(WindowOffset.Type.FOLLOWING); } ) + ) + ) + + { + return offset; + } } -Expression PreWhereClause(): +ExtractExpression ExtractExpression() : { - Expression retval = null; + ExtractExpression retval = new ExtractExpression(); + String fieldName = null; + Token token = null; + Expression expr = null; } { - retval=Expression() - { return retval; } + + "(" + ( fieldName=RelObjectName() { retval.setName(fieldName); } | token= { retval.setName(token.image); } ) + + expr=SimpleExpression() { retval.setExpression(expr); } + ")" + { + return retval; + } } -OracleHierarchicalExpression OracleHierarchicalQueryClause(): +CastExpression ImplicitCast() #ImplicitCast: { - OracleHierarchicalExpression result = new OracleHierarchicalExpression(); - Expression expr; + ColDataType colDataType; + Token tk1; + Token tk2; + + int precision = -1; + int scale = -1; + Token first = getToken(1); } { - ( - ( - expr=XorExpression() {result.setStartExpression(expr);} - [ { result.setNoCycle(true); } ] expr=XorExpression() - { - result.setConnectExpression(expr); - } - ) - | - ( - [ { result.setNoCycle(true); } ] expr=XorExpression() - { - result.setConnectExpression(expr); - result.setConnectFirst(true); - } - [ LOOKAHEAD(2) expr=XorExpression() {result.setStartExpression(expr);} ] - ) + colDataType = DataType() + ( + tk2 = + | + tk2 = + | + tk2 = ) { - return result; + CastExpression castExpression = new CastExpression(colDataType, tk2.image); + linkAST(castExpression.getLeftExpression(), tk2, tk2); + linkAST(castExpression, first); + return castExpression; } } -PreferringClause PreferringClause(): +CastExpression CastExpression(): { - Expression result = null; + Token keyword; + CastExpression retval; + ColumnDefinition columnDefinition; + ColDataType type; + Expression expression = null; + boolean useCastKeyword; + Token formatCharLiteral; } { - - result=PreferenceTerm() - + ( + keyword= + | + keyword= + | + keyword= + | + keyword= + ) { - PreferringClause preferring = new PreferringClause(result); - return preferring; + retval = new CastExpression(keyword.image); } -} + "(" + expression=SimpleExpression() + { retval.setUseCastKeyword(true); } + ( + LOOKAHEAD(2) ( + + "(" + columnDefinition=ColumnDefinition() { retval.addColumnDefinition(columnDefinition); } + ( "," columnDefinition=ColumnDefinition() { retval.addColumnDefinition(columnDefinition); } )* + ")" + ) + | + ( + type=ColDataType() { + // Preserve the established MySQL CAST type-name API outside PostgreSQL mode. + if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && type.isUsingArrayKeyword() && type.getArrayData().size() == 1 + && type.getArrayData().get(0) == null) { + type.setUsingArrayKeyword(false); + type.getArrayData().clear(); + type.setDataType(type.getDataType() + " ARRAY"); + } + retval.setColDataType(type); + } + // MySQL casts to an array of the given type when a multi-valued index key part is + // defined, e.g. CAST(data->'$.zips' AS UNSIGNED ARRAY). ARRAY reads as a further + // word of the type, the way INT UNSIGNED already does. + [ { type.setDataType(type.getDataType() + " ARRAY"); } ] + ) + ) -Expression PreferenceTerm(): -{ - Expression retval = null; -} -{ - // recursively build preferenceterm inside Plus - // like Expression -> XorExpression -> OrExpression -> ... + // BigQuery FORMAT clause + // https://cloud.google.com/bigquery/docs/reference/standard-sql/conversion_functions#cast_as_date + [ formatCharLiteral= { retval.setFormat(formatCharLiteral.image); } ] - retval=Plus() + ")" { + retval.setLeftExpression(expression); return retval; } } -Expression Plus(): +Expression CaseWhenExpression() #CaseWhenExpression: { - Expression left, right, result; + CaseExpression caseExp = new CaseExpression(); + Expression switchExp = null; + WhenClause clause; + List whenClauses = new ArrayList(); + Expression elseExp = null; } { - left=PriorTo() { result = left; } - ( - LOOKAHEAD(2) - - right=PriorTo() - { - result = new Plus(left, right); - left = result; - } - )* + { caseCounter++; } + [ LOOKAHEAD(2) switchExp=Expression() ] + ( clause=WhenThenSearchCondition() { whenClauses.add(clause); } )+ + [ + + ( + LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted}) elseExp=Expression() + | elseExp=SimpleExpression() + ) + ] + { caseCounter--; } { - return result; + caseExp.setSwitchExpression(switchExp); + caseExp.setWhenClauses(whenClauses); + caseExp.setElseExpression(elseExp); + return caseExp; } } -Expression PriorTo(): +WhenClause WhenThenSearchCondition(): { - Expression left, right, result; + WhenClause whenThen = new WhenClause(); + Expression whenExp; + Expression thenExp; + Token first = getToken(1); } { + whenExp=Expression() + ( - LOOKAHEAD(3) left=PreferenceTermTerminal() + LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted}) thenExp=Expression() | - "(" left=PreferenceTerm() ")" { left = new ParenthesedExpressionList(left); } + thenExp=SimpleExpression() ) - { result = left; } - - ( - LOOKAHEAD(2) - ( - LOOKAHEAD(3) right=PreferenceTermTerminal() - | - "(" right=PreferenceTerm() ")" { left = new ParenthesedExpressionList(right); } - ) - { - result = new PriorTo(left, right); - left = result; - } - )* { - return result; + whenThen.setWhenExpression(whenExp); + whenThen.setThenExpression(thenExp); + linkAST(whenThen, first); + return whenThen; } } -Expression PreferenceTermTerminal(): -{ - Expression retval; +RowConstructor RowConstructor(): { + Token token; + ParenthesedExpressionList expressions; +} { + token= + expressions = ParenthesedExpressionList() + { + return new RowConstructor(token.image, expressions); + } } -{ - ( - LOOKAHEAD(2) retval=HighExpression() - | - LOOKAHEAD(2) retval=LowExpression() - | - LOOKAHEAD(2) retval=Inverse() - | - retval=Condition() - ) +/** +TODO: VariableExpression should be a standalone class with more operations available. +*/ +EqualsTo VariableExpression(): { + Expression left; + Expression right; +} { + left = UserVariable() "=" right = SimpleExpression() { - return retval; + EqualsTo equals = new EqualsTo(); + equals.setLeftExpression(left); + equals.setRightExpression(right); + return equals; } } -Expression HighExpression() #HighExpression: -{ - Expression result; +Execute Execute(): { + ObjectNames funcName; + ExpressionList expressionList; + Execute execute = new Execute(); } { - - result=Expression() + ( { execute.setExecType(Execute.ExecType.EXEC); } + | { execute.setExecType(Execute.ExecType.EXECUTE); } + | { execute.setExecType(Execute.ExecType.CALL); } ) - { - HighExpression high = new HighExpression(result); - linkAST(high,jjtThis); - return high; - } + // A colon starts a bind argument here; it must not join the procedure name. + funcName=ColumnIdentifier() { execute.setName(funcName.getNames()); } + [ LOOKAHEAD(2) expressionList=ExecuteArguments() { execute.setExprList(expressionList); } ] + { return execute; } } -Expression LowExpression() #LowExpression: +ExpressionList ExecuteArguments(): { + ExpressionList arguments = new ExpressionList(); + Expression argument; +} { - Expression result; + ( + LOOKAHEAD(1, "(") arguments=ExpressionList() + | + argument=ExecuteArgument() { arguments.add(argument); } + ( LOOKAHEAD(2) "," argument=ExecuteArgument() { arguments.add(argument); } )* + ) + { return linkElements(arguments); } +} + +Expression ExecuteArgument(): { + Expression argument; + boolean output = false; } { - - result=Expression() + argument=Expression() + [ + LOOKAHEAD({ isAccessKeywordAhead("OUTPUT") || isAccessKeywordAhead("OUT") }) + ( | AccessKeyword("OUT") ) + { output = true; } + ] + { return output ? createOutputArgument(argument) : argument; } - { - LowExpression low = new LowExpression(result); - linkAST(low,jjtThis); - return low; - } } -Expression Inverse() #Inverse: -{ - Expression result; +FullTextSearch FullTextSearch() : { + Token searchModifier; + Expression againstValue; + FullTextSearch fs = new FullTextSearch(); + ExpressionList matchedColumns; } { - - "(" result=PreferenceTerm() ")" - + "(" matchedColumns=ColumnList() ")" + "(" + againstValue=SimpleExpression() { fs.setAgainstValue(againstValue); } + [ + ( + searchModifier="IN NATURAL LANGUAGE MODE" + | searchModifier="IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION" + | searchModifier="IN BOOLEAN MODE" + | searchModifier="WITH QUERY EXPANSION" + ) + { fs.setSearchModifier(searchModifier.image); } + ] + ")" { - Inverse inverse = new Inverse(result); - linkAST(inverse,jjtThis); - return inverse; + fs.setMatchColumns(matchedColumns); + return fs; } } -GroupByElement GroupByColumnReferences(): +LambdaExpression LambdaExpression() #LambdaExpression: { - Expression columnReference; - GroupByElement groupBy = new GroupByElement(); - Expression expr; - ExpressionList list; - Token token; + ExpressionList columns; + String s; + Expression expression; + LambdaExpression lambdaExpression; } { - ( - LOOKAHEAD(2) ( - - "(" - list = GroupingSet() { groupBy.addGroupingSet(list); } - ( LOOKAHEAD(2) "," list = GroupingSet() { groupBy.addGroupingSet(list); })* - ")" - ) - | - ( - list = ExpressionList() { groupBy.setGroupByExpressions(list); } - ( - LOOKAHEAD(2) - "(" - list = GroupingSet() { groupBy.addGroupingSet(list); } - ( LOOKAHEAD(2) "," list = GroupingSet() { groupBy.addGroupingSet(list); })* - ")" - )? - [ LOOKAHEAD(2) { groupBy.setMysqlWithRollup(true); } ] - ) + columns = ParenthesedColumnList() + | + s = RelObjectName() { columns = linkElements(new ExpressionList(linkToLastToken(createColumn(s)))); } ) + + "->" + expression = Expression() { - return groupBy; + lambdaExpression = LambdaExpression.from(columns, expression); + linkAST(lambdaExpression,jjtThis); + return lambdaExpression; } } -ExpressionList GroupingSet(): +/** + * DuckDB's lambda syntax, lambda x, y : expr, equivalent to (x, y) -> expr. + */ +LambdaExpression DuckDBLambdaExpression() #LambdaExpression: { - ExpressionList list; + List identifiers = new ArrayList(); + String s; Expression expression; + LambdaExpression lambdaExpression; } { - ( - LOOKAHEAD(2) list = ParenthesedExpressionList() - | - expression = SimpleExpression() { list = new ExpressionList(expression); } - ) + s = RelObjectName() { identifiers.add(s); } + ( "," s = RelObjectName() { identifiers.add(s); } )* + ":" + expression = Expression() { - return list; + lambdaExpression = new LambdaExpression(identifiers, expression).setLambdaKeyword(true); + linkAST(lambdaExpression,jjtThis); + return lambdaExpression; } } -Expression Having(): +RowPatternFunction RowPatternFunction() #RowPatternFunction: { - Expression having = null; + RowPatternFunction.EvaluationMode mode; + Function function; } { - having=Expression() + ( LOOKAHEAD({ isKeywordAhead("RUNNING") }) ContextualKeyword("RUNNING") + { mode = RowPatternFunction.EvaluationMode.RUNNING; } + | { mode = RowPatternFunction.EvaluationMode.FINAL; } + ) + function=Function() { - return having; + RowPatternFunction result = new RowPatternFunction(mode, function); + linkAST(result, jjtThis); return result; } } -Expression Qualify(): +Function Function() #Function: { - Expression qualify = null; + Function function; } { - qualify=Expression() + ( + "{" function = InternalFunction(true) "}" + { normalizeOdbcTimestampInterval(function); } + | LOOKAHEAD(3) function = SpecialStringFunctionWithNamedParameters() + | function = InternalFunction(false) + ) { - return qualify; + normalizeDateUnitArguments(function); + linkAST(function,jjtThis); + return function; } } -List OrderByElements(): +Function SpecialStringFunctionWithNamedParameters() : { - List orderByList = new ArrayList(); - OrderByElement orderByElement = null; + Token funcName; + NamedExpressionList namedExpressionList = null; + ExpressionList expressionList = null; + List orderByList; + Token open; } { - [ ] orderByElement=OrderByElement() { orderByList.add(orderByElement); } - ( LOOKAHEAD(2) "," orderByElement=OrderByElement() { orderByList.add(orderByElement); } )* + funcName = + + open="(" + ( + LOOKAHEAD( { isNamedExprListAhead() } ) namedExpressionList = NamedExpressionListExprFirst() + | + expressionList=ExpressionList() + ) + ")" + { - return orderByList; + if (namedExpressionList != null) { + linkAST(namedExpressionList, open); + } + return linked(new Function() + .withName(funcName.image) + .withNamedParameters(namedExpressionList) + .withParameters(expressionList), funcName); } } -OrderByElement OrderByElement(): +JAVACODE +List consumeKeywordArguments() { + List args = null; + while (isKeywordArgumentAhead()) { + String name = getNextToken().image; + ExpressionList exprList = SimpleExpressionList(); + if (args == null) { + args = new ArrayList(); + } + Expression value = exprList.size() == 1 + ? (Expression) exprList.get(0) + : exprList; + args.add(new Function.KeywordArgument(name, value)); + } + return args; +} + +Function InternalFunction(boolean escaped): { - OrderByElement orderByElement = new OrderByElement(); - Expression columnReference = null; - Token collateToken = null; + Token prefixToken = null; + Function retval = new Function(); + ObjectNames funcName; + ExpressionList expressionList = null; + ExpressionList chainedExpressionList = null; + KeepExpression keep = null; + Expression expr = null; + Expression attributeExpression = null; + Column attributeColumn = null; + Token attributeToken = null; + Limit limit; + List orderByList = null; + List keywordArgs = null; } { - columnReference = Expression() - [ LOOKAHEAD() (collateToken= | collateToken=) { columnReference = new CollateExpression(columnReference, collateToken.image); } ] - [ LOOKAHEAD(2) ( | ( { orderByElement.setAsc(false); } )) { orderByElement.setAscDescPresent(true); } ] - [ LOOKAHEAD(2) - [ LOOKAHEAD(2) ( - { orderByElement.setNullOrdering(OrderByElement.NullOrdering.NULLS_FIRST); } - | - { orderByElement.setNullOrdering(OrderByElement.NullOrdering.NULLS_LAST); } + [ LOOKAHEAD(2) prefixToken = ] + funcName = RelObjectNames() { if (prefixToken!=null) funcName.getNames().add(0, prefixToken.image ); retval.setName(funcName.getNames()); } + + "(" { consumeXmlLeadingKeyword(retval); } + [ + LOOKAHEAD(2) [ + LOOKAHEAD(2) ( + { retval.setDistinct(true); } + | { retval.setAllColumns(true); } + | { retval.setUnique(true); } ) ] + ( + LOOKAHEAD(3) expressionList = FunctionArgumentList(retval) + + | + // The probe above is syntactic and cannot see through the Pratt operator + // loop, so it fails on arguments such as "( 1 + a ) / ( 1 + b )" and used + // to fall through to Select(), reporting "Encountered ... + // was expecting ". Select() is only reachable here for a bare + // sub-select, so decide on that instead. + LOOKAHEAD({ !isUnparenthesizedSelectAhead() }) expressionList = FunctionArgumentList(retval) + + | + LOOKAHEAD({ !getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) expr = Select() { expressionList = linkElements(new ExpressionList(expr)); } + ) ] - [ LOOKAHEAD(2) { orderByElement.setMysqlWithRollup(true); } ] + + [ + ( + expr = Expression() { retval.setHavingClause( "MIN", expr ); } + | + expr = Expression() { retval.setHavingClause( "MAX", expr ); } + ) + ] + + [ + ( + { retval.setNullHandling(Function.NullHandling.IGNORE_NULLS); } + ) + | + ( + { retval.setNullHandling(Function.NullHandling.RESPECT_NULLS); } + ) + ] + + // BigQuery orders aggregate arguments after the null handling: + // ARRAY_AGG(expr [IGNORE NULLS] [ORDER BY key] [LIMIT n]) + [ + LOOKAHEAD(2) orderByList = OrderByElements() { retval.setOrderByElements(orderByList); } + ] + + [ + limit = PlainLimit() { retval.setLimit(limit); } + ] + { - orderByElement.setExpression(columnReference); - return orderByElement; + keywordArgs = consumeKeywordArguments(); } -} -JdbcParameter JdbcParameter() : { - Token tk; - JdbcParameter retval; -} -{ - ( tk="?" | tk= ) - { retval = new JdbcParameter(++jdbcParameterIndex, false, tk.image); } + ")" - [ LOOKAHEAD(2) token = { retval.setUseFixedIndex(true); retval.setIndex(Integer.valueOf(token.image)); } ] + [ + LOOKAHEAD(2) "(" chainedExpressionList = ExpressionList() ")" + ] - { return retval; } -} + [ + LOOKAHEAD(2) + ( + LOOKAHEAD({ getToken(1).kind == S_DOUBLE + && getToken(1).image.matches("\\.[0-9]+") }) + attributeToken= { + retval.setAttribute(new LongValue(attributeToken.image.substring(1))); + } + | + "." ( + // tricky lookahead since we do need to support the following constructs + // schema.f1().f2() - Function with Function Column + // schema.f1().f2.f3 - Function with Attribute Column + LOOKAHEAD( 6 ) attributeExpression=Function() { retval.setAttribute(attributeExpression); } + | + attributeColumn=Column() { retval.setAttribute(attributeColumn); } + ) + ) + ] + + [ LOOKAHEAD(2) ( + ( + + { + retval.setNullHandling(Function.NullHandling.IGNORE_NULLS); + retval.setIgnoreNullsOutside(true); + } + ) + | + ( + + { + retval.setNullHandling(Function.NullHandling.RESPECT_NULLS); + retval.setIgnoreNullsOutside(true); + } + ) + ) + ] + + [ LOOKAHEAD(2) keep = KeepExpression() ] -Limit LimitWithOffset() #LimitWithOffset: + { + retval.setEscaped(escaped); + retval.setParameters(expressionList); + retval.setChainedParameters(chainedExpressionList); + retval.setName(funcName.getNames()); + retval.setKeep(keep); + retval.setKeywordArguments(keywordArgs); + return retval; + } +} + +/** + * Argument list of a Function, extracted from InternalFunction() so that it can be + * reached from more than one lookahead guard without duplicating the expansion. + */ +ExpressionList FunctionArgumentList(Function retval): { - Limit limit = new Limit(); - Expression firstExpression; - Expression secondExpression; + ExpressionList expressionList = null; + List orderByList; + String onOverflowTruncate = null; + Token overflowToken = null; + Token extraKeywordToken; } { - - ( - LOOKAHEAD(3) firstExpression = ParenthesedSelect() - | - firstExpression = Expression() - ) - ( - // MySQL: LIMIT offset, row_count - "," - secondExpression = Expression() - { - limit.setOffset(firstExpression); - limit.setRowCount(secondExpression); - } - | - // PostgreSQL: LIMIT row_count - { - limit.setRowCount(firstExpression); - } - ) + { if (hasUnqualifiedName(retval, "XMLFOREST")) { return XmlForestArguments(); } } + [ LOOKAHEAD(2) extraKeywordToken = { retval.setExtraKeyword(extraKeywordToken.image); } ] + expressionList=ExpressionList() + // explicit lookahead: InternalFunction accepts ORDER BY after IGNORE NULLS too + [ LOOKAHEAD(2) orderByList = OrderByElements() { retval.setOrderByElements(orderByList); } ] + + // https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/LISTAGG.html + [ + ( overflowToken= | overflowToken= ) { onOverflowTruncate=overflowToken.image; } + [ + overflowToken = { onOverflowTruncate+= " " + overflowToken.image; } + [ ( overflowToken= | overflowToken= ) { onOverflowTruncate+=" " + overflowToken.image + " COUNT"; }] + ] + ] { retval.setOnOverflowTruncate(onOverflowTruncate); } + { - linkAST(limit, jjtThis); - return limit; + return expressionList; } } -Limit PlainLimit() #PlainLimit: +/** XMLFOREST keeps expressions in the ordinary function parameter list. */ +ExpressionList XmlForestArguments(): { - Limit limit = new Limit(); - Expression rowCountExpression; + ExpressionList arguments = new ExpressionList(); + Expression expression; } { - // mysql-postgresql-> LIMIT (row_count | ALL | NULL) - - ( - LOOKAHEAD(3) rowCountExpression = ParenthesedSelect() - | - rowCountExpression = Expression() - ) - { - limit.setRowCount(rowCountExpression); - linkAST(limit,jjtThis); - return limit; - } + expression=XmlForestArgument() { arguments.add(expression); } + ( "," expression=XmlForestArgument() { arguments.add(expression); } )* + { return linkElements(arguments); } } -/** - * Clickhouse LIMIT BY - * @see SELECT Query - */ -Limit LimitBy(): +Expression XmlForestArgument(): { - Limit limit; - ExpressionList byExpressions; + Expression expression; + String name; } { - limit = LimitWithOffset() - byExpressions = ExpressionList() - { - limit.setByExpressions(byExpressions); - return limit; - } + expression=Expression() + [ name=RelObjectName() { expression = new AliasedExpression(expression, new Alias(name, true)); } ] + { return expression; } } -Offset Offset(): -{ - Offset offset = new Offset(); - Expression offsetExpression; +XMLSerializeExpr XMLSerializeExpr(): { + XMLSerializeExpr result = new XMLSerializeExpr(); + Expression expression; + List orderByElements = null; + ColDataType dataType; + Token token; } { + "(" ( - // postgresql-> OFFSET offset - // sqlserver-oracle-> OFFSET offset (ROW | ROWS) - - offsetExpression=Expression() { offset.setOffset( offsetExpression ); } + LOOKAHEAD({ isKeywordAhead("CONTENT") || isKeywordAhead("DOCUMENT") }) + token= + { result.setSerializationMode(XMLSerializeExpr.SerializationMode.valueOf(token.image.toUpperCase(Locale.ROOT))); } + expression=Expression() { result.setExpression(expression); } + [ dataType=ColDataType() { result.setDataType(dataType); } ] + [ token= { result.setEncoding(stringValue(token)); } ] + [ token= { result.setVersion(stringValue(token)); } ] + [ + LOOKAHEAD(2) token= { + requireDdlSyntax("INDENT".equalsIgnoreCase(token.image), "Expected INDENT after NO"); + result.setIndent(false); + } + | + LOOKAHEAD({ isKeywordAhead("INDENT") }) token= { result.setIndent(true); } + [ "=" token= { result.setIndentSize(new LongValue(token.image)); } ] + ] + [ + { result.setShowDefaults(true); } + | + LOOKAHEAD({ isKeywordAhead("HIDE") }) token= + { result.setShowDefaults(false); } + ] + | + + "(" + "(" expression=SimpleExpression() ")" + [ orderByElements=OrderByElements() ] + ")" + dataType=ColDataType() { + result.setExpression(expression); + result.setOrderByElements(orderByElements); + result.setDataType(dataType); + } + ) + ")" + { return result; } +} - [ LOOKAHEAD(2) ( { offset.setOffsetParam("ROWS"); } | { offset.setOffsetParam("ROW"); })] - ) +JsonTableFunction.JsonTablePassingClause JsonTablePassingClause() : { + Expression valueExpression; + String parameterName; + Token first = getToken(1); +} +{ + valueExpression = Expression() + + parameterName = RelObjectName() { - return offset; + return linked(new JsonTableFunction.JsonTablePassingClause(valueExpression, parameterName), first); } } -Fetch Fetch(): +/** Compatibility entry point; both column response clauses use the same behaviors. */ +JsonFunction.JsonOnResponseBehavior JsonTableOnEmptyBehavior() : { + JsonFunction.JsonOnResponseBehavior behavior; +} { - Fetch fetch = new Fetch(); - Token token = null; + behavior = JsonTableOnResponseBehavior() + { return behavior; } +} + +JsonFunction.JsonOnResponseBehavior JsonTableOnResponseBehavior() : { + JsonFunction.JsonOnResponseBehavior behavior; Expression expression; - List fetchParameters = new ArrayList(); } { ( - LOOKAHEAD(3) ( - ( { fetch.setFetchParamFirst(true); } | ) - ( - { fetch.addFetchParameter("ROWS"); } - | - { fetch.addFetchParameter("ROW"); } - ) - ( - { fetch.addFetchParameter("ONLY"); } - | - { fetch.addFetchParameter("WITH TIES"); } - ) - ) + expression = Expression() + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.DEFAULT, expression); + } | - ( - ( { fetch.setFetchParamFirst(true); } | ) - - // Expression is optional according to https://www.h2database.com/html/commands.html#select - expression = Expression() { fetch.setExpression(expression); } - [ { fetch.addFetchParameter("PERCENT"); } ] - ( - ( - { fetch.addFetchParameter("ROWS"); } - | - { fetch.addFetchParameter("ROW"); } - ) - - ( - { fetch.addFetchParameter("ONLY"); } - | - { fetch.addFetchParameter("WITH TIES"); } - ) - ) - ) + behavior = JsonQueryOnResponseBehavior() ) - { - return fetch; - } + { return behavior; } } -WithIsolation WithIsolation(): -{ - WithIsolation withIsolation = new WithIsolation(); - Token token = null; - JdbcParameter jdbc; +JsonTableFunction.JsonTableWrapperClause JsonTableWrapperClause(boolean beforePathExpr) : { + JsonTableFunction.JsonTableWrapperClause wrapperClause = + new JsonTableFunction.JsonTableWrapperClause(beforePathExpr); + Token token; + Token first = getToken(1); } { - - token= - { withIsolation.setIsolation(token.image); - return withIsolation; + ( + { + wrapperClause.setWrapperType(JsonFunction.JsonWrapperType.WITHOUT); + } + | + { + wrapperClause.setWrapperType(JsonFunction.JsonWrapperType.WITH); + } + [ + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && (getToken(1).image.equalsIgnoreCase("CONDITIONAL") + || getToken(1).image.equalsIgnoreCase("UNCONDITIONAL")) + }) + token = + { + if (token.image.equalsIgnoreCase("CONDITIONAL")) { + wrapperClause.setWrapperMode(JsonFunction.JsonWrapperMode.CONDITIONAL); + } else { + wrapperClause.setWrapperMode(JsonFunction.JsonWrapperMode.UNCONDITIONAL); + } + } + ] + ) + [ { wrapperClause.setArray(true); } ] + JsonKeyword("WRAPPER") + { + return linked(wrapperClause, first); } } -OptimizeFor OptimizeFor(): -{ - Token token; - LongValue value; +JsonTableFunction.JsonTableQuotesClause JsonTableQuotesClause() : { + JsonTableFunction.JsonTableQuotesClause quotesClause = + new JsonTableFunction.JsonTableQuotesClause(); + Token first = getToken(1); } { - token= + ( + { quotesClause.setQuotesType(JsonFunction.JsonQuotesType.KEEP); } + | + JsonKeyword("OMIT") { quotesClause.setQuotesType(JsonFunction.JsonQuotesType.OMIT); } + ) + JsonKeyword("QUOTES") + [ + JsonKeyword("SCALAR") { quotesClause.setOnScalarString(true); } + ] { - value = new LongValue(token.image); - return new OptimizeFor(value.getValue()); + return linked(quotesClause, first); } } -// according to http://technet.microsoft.com/en-us/library/ms189463.aspx -Top Top() #Top: -{ - Top top = new Top(); - Token token = null; - Expression expr = null; - JdbcParameter jdbc = null; +JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { + JsonTableFunction.JsonTableColumnDefinition columnDefinition = null; + JsonTableFunction.JsonTableNestedColumnDefinition nestedColumnDefinition; + JsonTableFunction.JsonTableValueColumnDefinition valueColumnDefinition; + String columnName; + ColDataType dataType; + Expression expression; + String pathName = null; + JsonTableFunction.JsonTableColumnsClause columnsClause; + JsonFunction.JsonOnResponseBehavior behavior; + JsonTableFunction.JsonTableWrapperClause wrapperClause; + JsonTableFunction.JsonTableQuotesClause quotesClause; + String encoding; + String collation; + Token collationLiteral; + Token first = getToken(1); } { - ( - token= { top.setExpression(new LongValue(token.image)); } - | - jdbc = JdbcParameter() { top.setExpression(jdbc); } - | - ":" { top.setExpression(new JdbcNamedParameter()); } - [ LOOKAHEAD(2) token = { ((JdbcNamedParameter)top.getExpression()).setName(token.image); } ] + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("NESTED") }) + JsonKeyword("NESTED") + { nestedColumnDefinition = new JsonTableFunction.JsonTableNestedColumnDefinition(); } + [ LOOKAHEAD(2) { nestedColumnDefinition.setPathKeyword(true); } ] + expression = Expression() { nestedColumnDefinition.setPathExpression(expression); } + [ pathName = RelObjectName() { nestedColumnDefinition.setPathName(pathName); } ] + columnsClause = JsonTableColumnsClause() { + nestedColumnDefinition.setColumnsClause(columnsClause); + columnDefinition = nestedColumnDefinition; + } | - "(" - expr=SimpleExpression() - { - top.setExpression(expr); - top.setParenthesis(true); + columnName = RelObjectName() { + valueColumnDefinition = new JsonTableFunction.JsonTableValueColumnDefinition(); + valueColumnDefinition.setColumnName(columnName); + columnDefinition = valueColumnDefinition; } - ")" + ( + { valueColumnDefinition.setForOrdinality(true); } + | + [ + // Very ugly: ColDataType can consume an IDENTIFIER, which is fine, but we don't want it to + // consume an ALLOW or DISALLOW because that's a keyword for Oracle in this place. + // So we make a LOOKAHEAD on ColDataType, BUT we exclude the two cases for the IDENTIFIER + + /* + [ ColDataType ] + [ FORMAT JSON ] + [ (ALLOW | DISALLOW) SCALARS ] + */ + + LOOKAHEAD( + ColDataType(), + { getToken(1).kind != K_PATH + && !(getToken(1).kind == S_IDENTIFIER && ( + getToken(1).image.equalsIgnoreCase("ALLOW") + || getToken(1).image.equalsIgnoreCase("DISALLOW"))) } ) + dataType = ColDataType() { valueColumnDefinition.setDataType(dataType); } + [ LOOKAHEAD({ Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_COLLATE }) + + ( collation = RelObjectName() + | collationLiteral = { collation = collationLiteral.image; } ) + { valueColumnDefinition.setCollation(collation); } + ] + ] + [ + { valueColumnDefinition.setFormatJson(true); } + [ encoding = JsonEncoding() { valueColumnDefinition.setEncoding(encoding); } ] + ] + [ + ( + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("ALLOW") }) + JsonKeyword("ALLOW") { valueColumnDefinition.setScalarsType(JsonFunction.ScalarsType.ALLOW); } + | + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("DISALLOW") }) + JsonKeyword("DISALLOW") { valueColumnDefinition.setScalarsType(JsonFunction.ScalarsType.DISALLOW); } + ) + JsonKeyword("SCALARS") + ] + [ { valueColumnDefinition.setExistsKeyword(true); } ] + // In Oracle, the wrapper clause comes before the PATH expression + [ LOOKAHEAD(2) wrapperClause = JsonTableWrapperClause(true) { valueColumnDefinition.setWrapperClause(wrapperClause); } ] + [ expression = Expression() { valueColumnDefinition.setPathExpression(expression); } ] + // In Truno the wrapper clause comes after the PATH expression + [ wrapperClause = JsonTableWrapperClause(false) { valueColumnDefinition.setWrapperClause(wrapperClause); } ] + [ + LOOKAHEAD({ + getToken(1).kind == K_KEEP + || (getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("OMIT")) + }) + quotesClause = JsonTableQuotesClause() { valueColumnDefinition.setQuotesClause(quotesClause); } + ] + [ + LOOKAHEAD( JsonTableOnResponseBehavior() ) + behavior = JsonTableOnResponseBehavior() + + { valueColumnDefinition.setOnEmptyBehavior(behavior); } + ] + [ + LOOKAHEAD( JsonTableOnResponseBehavior() ) + behavior = JsonTableOnResponseBehavior() + + { valueColumnDefinition.setOnErrorBehavior(behavior); } + ] + [ + LOOKAHEAD( JsonTableOnResponseBehavior() ) + behavior = JsonTableOnResponseBehavior() + + { + valueColumnDefinition.setOnEmptyBehavior(behavior); + valueColumnDefinition.setOnEmptyAfterOnError(true); + } + ] + ) ) - [ LOOKAHEAD(2) { top.setPercentage(true); } ] - [ LOOKAHEAD(2) { top.setWithTies(true); } ] { - linkAST(top,jjtThis); - return top; + return linked(columnDefinition, first); } } -// according to http://www-01.ibm.com/support/knowledgecenter/SSGU8G_12.1.0/com.ibm.sqls.doc/ids_sqs_0156.htm -Skip Skip(): -{ - Skip skip = new Skip(); - Token token = null; - JdbcParameter jdbc; +JsonTableFunction.JsonTableColumnsClause JsonTableColumnsClause() : { + JsonTableFunction.JsonTableColumnsClause columnsClause = + new JsonTableFunction.JsonTableColumnsClause(); + JsonTableFunction.JsonTableColumnDefinition columnDefinition; + Token first = getToken(1); } { - - ( - token= { skip.setRowCount(Long.parseLong(token.image)); } - | - token= { skip.setVariable(token.image); } - | - jdbc = JdbcParameter() { skip.setJdbcParameter(jdbc); } - ) + "(" + [ + columnDefinition = JsonTableColumnDefinition() { + columnsClause.addColumnDefinition(columnDefinition); + } + ( + "," + columnDefinition = JsonTableColumnDefinition() { + columnsClause.addColumnDefinition(columnDefinition); + } + )* + ] + ")" { - return skip; - } -} - -JAVACODE -OracleHint getOracleHint() { - OracleHint hint = null; - Token tok = getToken(1); - // Retrieve first comment (if any) prior next token - if (tok.specialToken != null) { - tok = tok.specialToken; - while (tok.specialToken != null) tok = tok.specialToken; - // Check if it matches Hint pattern? - if (OracleHint.isHintMatch(tok.image)) { - hint = new OracleHint(); - hint.setComment(tok.image); + return linked(columnsClause, first); } - } - return hint; } -First First(): -{ - First first = new First(); - Token token = null; - JdbcParameter jdbc; +JsonTableFunction.JsonTablePlanTerm JsonTablePlanTerm() : { + JsonTableFunction.JsonTablePlanTerm term = null; + String value; + Expression expression; + JsonTableFunction.JsonTablePlanExpression nestedPlanExpression; + Token first = getToken(1); } { ( - { first.setKeyword(First.Keyword.FIRST); } - | - { first.setKeyword(First.Keyword.LIMIT); } - ) - ( - token= { first.setRowCount(Long.parseLong(token.image)); } + LOOKAHEAD(2) + "(" nestedPlanExpression = JsonTablePlanExpression() ")" { + term = new JsonTableFunction.JsonTablePlanTerm(); + term.setNestedPlanExpression(nestedPlanExpression); + } | - token= { first.setVariable(token.image); } + LOOKAHEAD(2) + value = RelObjectName() { + term = new JsonTableFunction.JsonTablePlanTerm(); + term.setName(value); + } | - jdbc = JdbcParameter() { first.setJdbcParameter(jdbc); } + expression = Expression() { + term = new JsonTableFunction.JsonTablePlanTerm(); + term.setExpression(expression); + } ) { - return first; - } -} - - -Expression Expression() #Expression : -{ - Expression expression = null; -} -{ - expression=Condition() - { expression = prattExpressionRest(expression, 2); } - { - linkAST(expression,jjtThis); - return expression; + return linked(term, first); } } -Expression XorExpression(): -{ - Expression result; - Expression right; +JsonTableFunction.JsonTablePlanExpression JsonTablePlanExpression() : { + JsonTableFunction.JsonTablePlanExpression planExpression = + new JsonTableFunction.JsonTablePlanExpression(); + JsonTableFunction.JsonTablePlanTerm term; + Token operator = null; + Token first = getToken(1); } { - result=AndChain() + term = JsonTablePlanTerm() { planExpression.addTerm(term); } ( - LOOKAHEAD(2) ( - right=AndChain() { result = new OrExpression(result, right); } + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.COMMA); + } | - right=AndChain() { result = new XorExpression(result, right); } + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.INNER); + } + | + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.OUTER); + } + | + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.CROSS); + } + | + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.UNION); + } ) + term = JsonTablePlanTerm() { planExpression.addTerm(term); } )* - { return result; } + { + return linked(planExpression, first); + } } -/** AND — higher precedence than OR/XOR */ -Expression AndChain(): -{ - Expression result; - Expression right; +JsonTableFunction.JsonTablePlanClause JsonTablePlanClause() : { + JsonTableFunction.JsonTablePlanClause planClause = + new JsonTableFunction.JsonTablePlanClause(); + JsonTableFunction.JsonTablePlanExpression planExpression; + Token first = getToken(1); } { - result=Condition() - ( - LOOKAHEAD(2) - ( - right=Condition() {{ - AndExpression a = new AndExpression(result, right); result = a; - }} - | - right=Condition() {{ - AndExpression a = new AndExpression(result, right); - a.setUseOperator(true); result = a; - }} - ) - )* - { return result; } + + [ { planClause.setDefaultPlan(true); } ] + "(" planExpression = JsonTablePlanExpression() ")" { planClause.setPlanExpression(planExpression); } + { + return linked(planClause, first); + } } -Expression Condition(): -{ - Expression result; - Expression left; - boolean not = false; - boolean exclamationMarkNot = false; - int oraclePrior = EqualsTo.NO_ORACLE_PRIOR; - int oracleJoin = EqualsTo.NO_ORACLE_JOIN; +JsonTableFunction.JsonTableOnErrorClause JsonTableOnErrorClause(boolean beforeColumns) : { + JsonTableFunction.JsonTableOnErrorClause onErrorClause = + new JsonTableFunction.JsonTableOnErrorClause(beforeColumns); + Token token; + Token first = getToken(1); } { - [ LOOKAHEAD(2, { getToken(1).kind == K_NOT || "!".equals(getToken(1).image) }) ( { not=true; } | "!" { not=true; exclamationMarkNot=true; })] ( - result=ExistsExpression() + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.ERROR); } | - [ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] - left=SimpleExpression() { result = left; } - - // Consume Oracle (+) once, before dispatching - [ - LOOKAHEAD("(" "+" ")", { getToken(1).kind == OPENING_BRACKET }) - "(" "+" ")" - { - oracleJoin = EqualsTo.ORACLE_JOIN_RIGHT; - if (left instanceof Column) { - ((Column) left).setOldOracleJoinSyntax(oracleJoin); - } - } - ] - - // Single guard: only enter if next token can start a condition suffix - [ - LOOKAHEAD({ isConditionSuffixAhead() }) - ( - LOOKAHEAD({ isComparisonOperatorAhead() }) - result = RegularConditionRHS(left, oracleJoin) - | LOOKAHEAD(2) result = OverlapsCondition(left) - | LOOKAHEAD(3) result=InExpression(left) - | LOOKAHEAD(3) result=ExcludesExpression(left) - | LOOKAHEAD(3) result=IncludesExpression(left) - | LOOKAHEAD(2) result=Between(left) - | LOOKAHEAD(2) result = MemberOfExpression(left) - | LOOKAHEAD(3) result=IsNullExpression(left) - | LOOKAHEAD(3) result=IsBooleanExpression(left) - | LOOKAHEAD(3) result=IsUnknownExpression(left) - | LOOKAHEAD(2) result=LikeExpression(left) - | LOOKAHEAD(3) result=IsDistinctExpression(left) - | result=SimilarToExpression(left) - ) - ] + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.EMPTY); } + | + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.TRUE); } + | + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.FALSE); } + | + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.NULL); } ) + { - if (oraclePrior == EqualsTo.ORACLE_PRIOR_START - && result instanceof SupportsOldOracleJoinSyntax) { - ((SupportsOldOracleJoinSyntax) result).setOraclePriorPosition(oraclePrior); - } - return not ? new NotExpression(result, exclamationMarkNot) : result; + return linked(onErrorClause, first); } } -Expression RegularConditionRHS(Expression leftExpression, int oracleJoinRight) #RegularCondition: -{ - Expression result = null; - Expression rightExpression; - int oracleJoin = EqualsTo.NO_ORACLE_JOIN; - int oraclePrior = EqualsTo.NO_ORACLE_PRIOR; +JsonTableFunction.JsonTableOnEmptyClause JsonTableOnEmptyClause() : { + JsonTableFunction.JsonTableOnEmptyClause onEmptyClause = + new JsonTableFunction.JsonTableOnEmptyClause(); Token token; + Token first = getToken(1); } { - // Only consume (+) here if it wasn't already consumed by Condition - [ LOOKAHEAD("(" "+" ")") "(" "+" ")" { oracleJoin = EqualsTo.ORACLE_JOIN_RIGHT; } ] - ( - LOOKAHEAD(2) - ">" { result = new GreaterThan(); } - | "<" { result = new MinorThan(); } - | "=" { result = new EqualsTo(); } - | token= { result = new GreaterThanEquals(token.image); } - | token= { result = new MinorThanEquals(token.image); } - | token= { result = new NotEqualsTo(token.image); } - | token= { result = new NotEqualsTo(token.image); } - | token= { result = new NotEqualsTo(token.image); } - | "*=" { result = new TSQLLeftJoin(); } - | "=*" { result = new TSQLRightJoin(); } - | token= { result = new DoubleAnd(); } - | token= { result = new Contains(); } - | token= { result = new ContainedBy(); } - | "@@" { result = new Matches(); } - | "~" { result = new RegExpMatchOperator(RegExpMatchOperatorType.MATCH_CASESENSITIVE); } - | "~*" { result = new RegExpMatchOperator(RegExpMatchOperatorType.MATCH_CASEINSENSITIVE); } - | "!~" { result = new RegExpMatchOperator(RegExpMatchOperatorType.NOT_MATCH_CASESENSITIVE); } - | "!~*" { result = new RegExpMatchOperator(RegExpMatchOperatorType.NOT_MATCH_CASEINSENSITIVE); } - | "@>" { result = new JsonOperator("@>"); } - | "<@" { result = new JsonOperator("<@"); } - | "?" { result = new JsonOperator("?"); } - | "?|" { result = new JsonOperator("?|"); } - | "?&" { result = new JsonOperator("?&"); } - | { result = new JsonOperator("||"); } - | "-" { result = new JsonOperator("-"); } - | "-#" { result = new JsonOperator("-#"); } - | "<->" { result = new GeometryDistance("<->"); } - | "<#>" { result = new GeometryDistance("<#>"); } - | { result = new CosineSimilarity(); } + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.ERROR); } + | + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.EMPTY); } + | + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.TRUE); } + | + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.FALSE); } + | + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.NULL); } ) - - ( LOOKAHEAD(2) rightExpression=ComparisonItem() { oraclePrior = EqualsTo.ORACLE_PRIOR_END; } - | rightExpression=ComparisonItem() ) - - [ LOOKAHEAD(2) "(" "+" ")" { oracleJoin = EqualsTo.ORACLE_JOIN_LEFT; } ] - + { - BinaryExpression regCond = (BinaryExpression) result; - regCond.setLeftExpression(leftExpression); - regCond.setRightExpression(rightExpression); - - if (oracleJoin > 0) - ((SupportsOldOracleJoinSyntax) result).setOldOracleJoinSyntax(oracleJoin); - - if (oraclePrior != EqualsTo.NO_ORACLE_PRIOR) - ((SupportsOldOracleJoinSyntax) result).setOraclePriorPosition(oraclePrior); + return linked(onEmptyClause, first); } +} +JsonTableFunction.JsonTableParsingTypeClause JsonTableParsingTypeClause() : { + JsonTableFunction.JsonTableParsingTypeClause parsingType = new JsonTableFunction.JsonTableParsingTypeClause(); + Token first = getToken(1); +} +{ + + + ( + { parsingType.setType(JsonTableFunction.JsonTableParsingType.STRICT); } + | + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("LAX") }) + JsonKeyword("LAX") { parsingType.setType(JsonTableFunction.JsonTableParsingType.LAX); } + ) + { - linkAST(result, jjtThis); - return result; + return linked(parsingType, first); } } -Expression OverlapsCondition(Expression leftExpression): -{ - ExpressionList right; +JsonTableFunction JsonTableBody() : { + JsonTableFunction function = new JsonTableFunction(); + Expression jsonInput; + Expression jsonPath; + JsonTableFunction.JsonTablePassingClause passingClause; + String pathName = null; + JsonTableFunction.JsonTableColumnsClause columnsClause; + JsonTableFunction.JsonTablePlanClause planClause = null; + JsonTableFunction.JsonTableOnErrorClause onErrorClause = null; + JsonTableFunction.JsonTableParsingTypeClause parsingTypeClause = null; + JsonTableFunction.JsonTableOnEmptyClause onEmptyClause = null; } { - - right = ParenthesedExpressionList() - { - ExpressionList left; - if (leftExpression instanceof ExpressionList) { - left = (ExpressionList) leftExpression; - } else { - left = new ExpressionList(leftExpression); + "(" + jsonInput = Expression() { + function.setJsonInputExpression(jsonInput); + } + [ { function.setFormatJson(true); } ] + [ + "," + jsonPath = Expression() { + function.setJsonPathExpression(jsonPath); + function.setParameters(linkElements(new ExpressionList(jsonInput, jsonPath))); } - return new OverlapsCondition(left, right); + [ pathName = RelObjectName() { function.setPathName(pathName); } ] + ] + [ + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) + JsonKeyword("PASSING") + passingClause = JsonTablePassingClause() { function.addPassingClause(passingClause); } + ( + "," + passingClause = JsonTablePassingClause() { function.addPassingClause(passingClause); } + )* + ] + [ LOOKAHEAD(3) onErrorClause = JsonTableOnErrorClause(true) { function.setOnErrorClause(onErrorClause); } ] + [ parsingTypeClause = JsonTableParsingTypeClause() { function.setParsingTypeClause(parsingTypeClause); } ] + [ onEmptyClause = JsonTableOnEmptyClause() { function.setOnEmptyClause(onEmptyClause); } ] + columnsClause = JsonTableColumnsClause() { function.setColumnsClause(columnsClause); } + [ planClause = JsonTablePlanClause() { function.setPlanClause(planClause); } ] + [ onErrorClause = JsonTableOnErrorClause(false) { function.setOnErrorClause(onErrorClause); } ] + ")" + { + return function; } } -Expression SQLCondition(): -{ - Expression result; - Expression left; +XmlTableFunction.XmlTablePassingClause XmlTablePassingClause() : { + XmlTableFunction.XmlTablePassingClause passingClause = new XmlTableFunction.XmlTablePassingClause(); + Expression valueExpression; + String name = null; + Token first = getToken(1); } { - ( - result=ExistsExpression() - | left = SimpleExpression() { result = left; } - [ - LOOKAHEAD(2) ( - LOOKAHEAD(2, ) result=OverlapsCondition(left) - | - LOOKAHEAD(3, {!interrupted}) result=InExpression(left) - | - LOOKAHEAD(3) result=ExcludesExpression(left) - | - LOOKAHEAD(3) result=IncludesExpression(left) - | - LOOKAHEAD(2) result=Between(left) - | - result = MemberOfExpression(left) - | - LOOKAHEAD(3) result=IsNullExpression(left) - | - LOOKAHEAD(3) result=IsBooleanExpression(left) - | - LOOKAHEAD(3) result=IsUnknownExpression(left) - | - LOOKAHEAD(2) result=LikeExpression(left) - | - LOOKAHEAD(3) result=IsDistinctExpression(left) - | - result=SimilarToExpression(left) - ) - ] - ) - { return result; } + valueExpression = Expression() { passingClause.setValueExpression(valueExpression); } + [ name = RelObjectName() { passingClause.setName(name); } ] + { + return linked(passingClause, first); + } } -Expression InExpression(Expression leftExpression) #InExpression : -{ - Token token; - int oldOracleJoin = 0; - boolean usingNot = false; - boolean usingGlobal = false; - Expression rightExpression; +XmlTableFunction.XmlTableColumnDefinition XmlTableColumnDefinition() : { + XmlTableFunction.XmlTableColumnDefinition columnDefinition = new XmlTableFunction.XmlTableColumnDefinition(); + String columnName; + ColDataType dataType; + Expression expression; + Token first = getToken(1); } { - [ "(" "+" ")" { oldOracleJoin=EqualsTo.ORACLE_JOIN_RIGHT; } ] - - [ { usingGlobal=true; } ] - [ { usingNot=true; } ] - + columnName = RelObjectName() { columnDefinition.setColumnName(columnName); } ( - LOOKAHEAD(2) token= { rightExpression = new StringValue(token.image); } - | - LOOKAHEAD({ isNestedSetOperationAhead() }) rightExpression = ParenthesedSelect() + { columnDefinition.setForOrdinality(true); } | - rightExpression = PrimaryExpression() + dataType = ColDataType() { columnDefinition.setDataType(dataType); } + [ expression = Expression() { columnDefinition.setPathExpression(expression); } ] + [ expression = Expression() { columnDefinition.setDefaultExpression(expression); } ] ) { - InExpression inExpression = new InExpression(leftExpression, rightExpression) - .withOldOracleJoinSyntax(oldOracleJoin) - .withNot(usingNot) - .setGlobal(usingGlobal); - linkAST(inExpression,jjtThis); - return inExpression; + return linked(columnDefinition, first); } } -Expression IncludesExpression(Expression leftExpression) #IncludesExpression : -{ - Token token; - Expression rightExpression; +XmlTableFunction XmlTableBody() : { + XmlTableFunction function = new XmlTableFunction(); + Expression rowPath; + XmlTableFunction.XmlTablePassingClause passingClause; + XmlTableFunction.XmlTableColumnDefinition columnDefinition; } { - (rightExpression = ParenthesedExpressionList()) + "(" + rowPath = Expression() { function.setRowPathExpression(rowPath); } + [ + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) + + passingClause = XmlTablePassingClause() { function.addPassingClause(passingClause); } + ( + "," + passingClause = XmlTablePassingClause() { function.addPassingClause(passingClause); } + )* + ] + [ + + columnDefinition = XmlTableColumnDefinition() { function.addColumnDefinition(columnDefinition); } + ( + "," + columnDefinition = XmlTableColumnDefinition() { function.addColumnDefinition(columnDefinition); } + )* + ] + ")" { - IncludesExpression includesExpression = new IncludesExpression(leftExpression, rightExpression); - - linkAST(includesExpression,jjtThis); - return includesExpression; + return function; } } -Expression ExcludesExpression(Expression leftExpression) #ExcludesExpression : +TableFunction TableFunction(): { - Token token; - Expression rightExpression; + Token prefix = null; + Function function = null; + ParenthesedExpressionList rowsFromFunctions = null; + Token withClause = null; + Token keyword; } { - (rightExpression = ParenthesedExpressionList()) + [ prefix = ] + ( + LOOKAHEAD(3) rowsFromFunctions = RowsFromFunctionList() + | + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_TABLE") + }) + JsonKeyword("JSON_TABLE") { keyword = getToken(0); } + function = JsonTableBody() { linkAST(function, keyword); } + | + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("XMLTABLE") + }) + keyword= + function = XmlTableBody() { linkAST(function, keyword); } + | + function=Function() + ) + [ LOOKAHEAD(2) ( withClause = | withClause = ) ] { - ExcludesExpression excludesExpression = new ExcludesExpression(leftExpression, rightExpression); + if (rowsFromFunctions != null) { + return prefix != null + ? withClause != null + ? new TableFunction(prefix.image, rowsFromFunctions, withClause.image) + : new TableFunction(prefix.image, rowsFromFunctions) + : withClause != null + ? new TableFunction(rowsFromFunctions, withClause.image) + : new TableFunction(rowsFromFunctions); + } - linkAST(excludesExpression,jjtThis); - return excludesExpression; + return prefix != null + ? withClause != null + ? new TableFunction(prefix.image, function, withClause.image) + : new TableFunction(prefix.image, function) + : withClause != null + ? new TableFunction(function, withClause.image) + : new TableFunction(function); } } -Expression Between(Expression leftExpression) : +ParenthesedExpressionList RowsFromFunctionList(): { - Between result = new Between(); - Expression betweenExpressionStart = null; - Expression betweenExpressionEnd = null; + ParenthesedExpressionList functions = new ParenthesedExpressionList(); + Function function; + Token first = getToken(1); } { - [ { result.setNot(true); }] - - [ - LOOKAHEAD(2) ( - { result.setUsingSymmetric(true); } - | - { result.setUsingAsymmetric(true); } - ) - ] - ( - LOOKAHEAD({ isParenthesedSelectAhead() }) betweenExpressionStart = ParenthesedSelect() - | - betweenExpressionStart = SimpleExpression() - [ - LOOKAHEAD({ isComparisonOperatorAhead() }) - betweenExpressionStart = RegularConditionRHS(betweenExpressionStart, EqualsTo.NO_ORACLE_JOIN) - ] - ) - - + "(" + function = RowsFromFunction() { functions.add(function); } ( - LOOKAHEAD({ isParenthesedSelectAhead() }) betweenExpressionEnd = ParenthesedSelect() - | - betweenExpressionEnd = SimpleExpression() - [ - LOOKAHEAD({ isComparisonOperatorAhead() }) - betweenExpressionEnd = RegularConditionRHS(betweenExpressionEnd, EqualsTo.NO_ORACLE_JOIN) - ] - ) - + "," + function = RowsFromFunction() { functions.add(function); } + )* + ")" { - result.setLeftExpression(leftExpression); - result.setBetweenExpressionStart(betweenExpressionStart); - result.setBetweenExpressionEnd(betweenExpressionEnd); - return result; + linkAST(functions, first); + return functions; } } -Expression LikeExpression(Expression leftExpression) #LikeExpression: +/** A function and its optional PostgreSQL record column definition list. */ +Function RowsFromFunction(): { - LikeExpression result = new LikeExpression(); - Expression rightExpression = null; - Expression escape; - Token token; + Function function; + List columns = new ArrayList(); + ColumnDefinition column; } { - [ { result.setNot(true); } ] - ( - token = - | token = - | token = - | token = - | token = - | token = - | token = - | token = - | token = - | token = - | token = - ) { result.setLikeKeyWord( LikeExpression.KeyWord.from(token.image)); } - [ LOOKAHEAD(2) {result.setUseBinary(true); } ] - rightExpression=SimpleExpression() - [ LOOKAHEAD(2) - ( - LOOKAHEAD(2) token = { result.setEscape( new StringValue( token.image ) ); } - | - escape=Expression() { result.setEscape(escape); } - ) + function=Function() + [ "(" + column=RowsFromColumnDefinition() { columns.add(column); } + ( "," column=RowsFromColumnDefinition() { columns.add(column); } )* + ")" { function.setResultColumnDefinitions(columns); } ] - { - result.setLeftExpression(leftExpression); - result.setRightExpression(rightExpression); - linkAST(result,jjtThis); - return result; - } + { return function; } } -Expression SimilarToExpression(Expression leftExpression) #SimilarToExpression: +ColumnDefinition RowsFromColumnDefinition(): { - SimilarToExpression result = new SimilarToExpression(); - Expression rightExpression = null; + String name; + String collation; + String part; + ColDataType type; + ColumnDefinition column; } { - [ { result.setNot(true); } ] - - rightExpression=SimpleExpression() - [ LOOKAHEAD(2) token= { result.setEscape((new StringValue(token.image)).getValue()); }] - { - result.setLeftExpression(leftExpression); - result.setRightExpression(rightExpression); - linkAST(result,jjtThis); - return result; - } + name=RelObjectName() type=ColDataType() + { column = new ColumnDefinition(name, type); } + [ collation=RelObjectName() + ( "." part=RelObjectName() { collation += "." + part; } )* + { column.addColumnOptions(ColumnOption.collate(collation)); } + ] + { return column; } } -Expression IsDistinctExpression(Expression leftExpression) #IsDistinctExpression: -{ - IsDistinctExpression result = new IsDistinctExpression(); - Expression rightExpression = null; +List ColumnNamesWithParamsList() : { + List colNames = new ArrayList(); + String columnName; + List parameter = null; } { - [ { result.setNot(true); } ] - rightExpression=SimpleExpression() + "(" + columnName=RelObjectName() + + { parameter = null; } + [ parameter = CreateParameter() ] { - result.setLeftExpression(leftExpression); - result.setRightExpression(rightExpression); - linkAST(result,jjtThis); - return result; + colNames.add(new KeyElement(columnName,parameter)); } + + ( + "," + columnName=RelObjectName() + + { parameter = null; } + [ parameter = CreateParameter() ] + { + colNames.add(new KeyElement(columnName,parameter)); + } + )* + + ")" + + { return colNames; } } -Expression IsNullExpression(Expression leftExpression): -{ - IsNullExpression result = new IsNullExpression(); +KeyElement IndexColumnWithParams(): { + String columnName = null; + List columnParams = new ArrayList(); + List parameter = null; + Expression expression = null; + String collation = null; + String operatorClass = null; + List operatorClassParameters = null; + Token sortOrder = null; + Token nullOrdering = null; + KeyElement column = null; + boolean postgres = Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)); + boolean expressionParenthesized = true; } { ( - [ { result.setNot(true); } ] { result.setUseIsNull(true); } - | { result.setUseIsNull(true); result.setUseNotNull(true); } - | [ { result.setNot(true); } ] + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) && isFunctionAhead() }) expression=Function() + { expressionParenthesized = false; } + | + columnName=RelObjectName() + | + "(" expression=Expression() ")" ) - { - result.setLeftExpression(leftExpression); - return result; + // MySQL prefix lengths precede all PostgreSQL key attributes. + [ LOOKAHEAD("(", { !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) }) parameter=CreateParameter() { columnParams.addAll(parameter); } ] + [ LOOKAHEAD(2) collation=IndexKeyAttributeName() + { if (!postgres) { columnParams.add("COLLATE"); columnParams.add(collation); } } ] + [ LOOKAHEAD(2, IndexKeyAttributeName(), { + getToken(1).kind == S_IDENTIFIER || getToken(1).kind == S_QUOTED_IDENTIFIER + || (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind != K_ASC && getToken(1).kind != K_DESC + && getToken(1).kind != K_NULLS && getToken(1).kind != K_COLLATE + && getToken(1).kind != K_WITH && getToken(1).kind != K_WITHOUT) }) + operatorClass=IndexKeyAttributeName() + [ LOOKAHEAD(2) operatorClassParameters=PostgreSqlOptions(false) ] + { + if (!postgres) { + columnParams.add(operatorClass); + if (operatorClassParameters != null) { + columnParams.add(PlainSelect.getStringList( + operatorClassParameters, true, true)); + } + } + } + ] + [ LOOKAHEAD(2) (sortOrder= | sortOrder=) + { if (!postgres) { columnParams.add(sortOrder.image); } } ] + [ LOOKAHEAD(2) (nullOrdering= | nullOrdering=) + { if (!postgres) { columnParams.add("NULLS"); columnParams.add(nullOrdering.image); } } ] + ( LOOKAHEAD(2, { !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) && getToken(1).kind != K_WITH && getToken(1).kind != K_WITHOUT }) parameter=CreateParameter() { columnParams.addAll(parameter); } )* + { + column = expression != null + ? new KeyElement(expression, + columnParams.isEmpty() ? null : columnParams) + : new KeyElement(columnName, + columnParams.isEmpty() ? null : columnParams); + column.setCollation(collation); + column.setExpressionParenthesized(expressionParenthesized); + if (operatorClass != null) { + column.setOperatorClass(operatorClass); + column.setOperatorClassParameters(operatorClassParameters); + } + if (sortOrder != null) { + column.setSortOrder(KeyElement.SortOrder.valueOf( + sortOrder.image.toUpperCase(Locale.ROOT))); + } + if (nullOrdering != null) { + column.setNullOrdering(KeyElement.NullOrdering.valueOf( + nullOrdering.image.toUpperCase(Locale.ROOT))); + } + return column; } } -Expression IsBooleanExpression(Expression leftExpression): +/** Keeps qualified PostgreSQL collation and operator-class names out of expression parsing. */ +String IndexKeyAttributeName(): { - IsBooleanExpression result = new IsBooleanExpression(); + String name; + String part; } { - ( - [ { result.setNot(true); } ] ( { result.setIsTrue(true); } | { result.setIsTrue(false); }) - ) - - { - result.setLeftExpression(leftExpression); - return result; - } + name=RelObjectName() + ( LOOKAHEAD(".", { Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) }) + "." part=RelObjectNameExt() { name += "." + part; } + )* + { return name; } } -Expression IsUnknownExpression(Expression leftExpression): +IndexOption PostgreSqlIndexOption(boolean allowBare): { - IsUnknownExpression result = new IsUnknownExpression(); + String name = null; + Expression value = null; + boolean useEquals = false; } { - ( - [ { result.setNot(true); } ] - ) - + name=PostgreSqlStorageParameterName() + [ [ "=" { useEquals = true; } ] value=Expression() ] { - result.setLeftExpression(leftExpression); - return result; + requireDdlSyntax(allowBare || value != null, + "Operator-class parameters require a value"); + requireDdlSyntax(value == null || useEquals + || !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)), + "PostgreSQL parameter values require an equals sign"); + return new IndexOption(name, value, useEquals); } } -Expression ExistsExpression(): -{ - ExistsExpression result = new ExistsExpression(); - Expression rightExpression = null; -} +/** Storage parameters can have namespaces, such as toast.autovacuum_enabled. */ +String PostgreSqlStorageParameterName(): +{ String name; String part; } { - rightExpression=SimpleExpression() - { - result.setRightExpression(rightExpression); - return result; - } + name=RelObjectName() + ( LOOKAHEAD(1, ".") "." part=RelObjectNameExt() { name += "." + part; } )* + { return name; } } -Expression MemberOfExpression(Expression leftExpression): +/** Storage parameters allow a bare name; operator-class parameters require a value. */ +List PostgreSqlIndexOptions(): +{ List options; } +{ options=PostgreSqlOptions(true) { return options; } } + +List PostgreSqlOptions(boolean allowBare): { - MemberOfExpression result; - Expression rightExpression = null; + List options = new ArrayList(); + IndexOption option = null; } { - rightExpression=Expression() - { - return new MemberOfExpression(leftExpression, rightExpression ); - } + "(" option=PostgreSqlIndexOption(allowBare) { options.add(option); } + ( "," option=PostgreSqlIndexOption(allowBare) { options.add(option); } )* ")" + { return options; } } -ExpressionList ExpressionList() #ExpressionList: -{ - ExpressionList expressionList; +List IndexColumnsWithParamsList(): +{ List columns; } +{ columns=IndexKeyColumns(false) { return columns; } } + +List IndexKeyColumns(boolean temporal) : { + List colNames = new ArrayList(); + KeyElement column = null; } { - ( - LOOKAHEAD(3, { !interrupted }) expressionList = ComplexExpressionList() - | - LOOKAHEAD(3) expressionList = SimpleExpressionList() - | - LOOKAHEAD(3) expressionList = ParenthesedExpressionList() - | - // Fallback, see isUnparenthesizedSelectAhead(): the three probes above are - // syntactic and cannot see through the Pratt operator loop, which lives in a - // Java action. They all fail on a bracketed operand of a larger expression, - // e.g. "( 1 + a ) / ( 1 + b )" or "( 1 + a )" - the third token is an operator - // where the probe expects "," or ")". Such input is still a valid expression - // list, so parse it as one instead of raising a ParseException. - LOOKAHEAD({ !interrupted }) expressionList = ComplexExpressionList() - | - expressionList = SimpleExpressionList() - ) + "(" + column=IndexColumnWithParams() { - // Avoid redundant ExpressionLists containing only one ParenthesedExpressionList - // return the Parenthesed Sub ExpressionList instead - // Same for ParenthesedExpressionList containing only 1 ExpressionList + colNames.add(column); + } - if ( expressionList.size() == 1 && expressionList.get(0) instanceof ExpressionList ) { - ExpressionList subList = (ExpressionList) expressionList.get(0); - if (expressionList instanceof ParenthesedExpressionList) { - if (subList instanceof ParenthesedExpressionList) { - return expressionList; - } else { - return new ParenthesedExpressionList(subList); - } - } else { - if (subList instanceof ParenthesedExpressionList) { - return new ParenthesedExpressionList(subList); - } else { - return subList; - } - } + ( + "," + column=IndexColumnWithParams() + { + colNames.add(column); } - return expressionList; - } + )* + + [ { + requireDdlSyntax(temporal, "WITHOUT OVERLAPS requires a PRIMARY KEY or UNIQUE constraint"); + column.setWithoutOverlaps(true); + } ] + ")" + + { return colNames; } } -ParenthesedExpressionList ParenthesedExpressionList(): +Index Index(): { + ObjectNames name; +} { - ExpressionList expressions=new ExpressionList(); + name= RelObjectNames() { return new Index().withName(name.getNames()).withType(""); } +} + +/** Parses dialect-specific index modifiers while retaining the legacy single type token. */ +Index CreateIndexModifiers(CreateIndex statement): +{ + Index index = new Index(); + Token token; + List parameter; + IndexOptions.Clustering clustering; } { - "(" ( - LOOKAHEAD({ !interrupted }) expressions = ComplexExpressionList() - | - expressions = SimpleExpressionList() - )? - ")" - { - return new ParenthesedExpressionList(expressions); - } + LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + && (getToken(1).kind == K_UNIQUE || isKeywordAhead("CLUSTERED") + || isKeywordAhead("NONCLUSTERED")) }) + [ token= { index.setType(token.image); } ] + clustering=SqlServerIndexClustering() { index.setClustering(clustering); } + | + LOOKAHEAD({ Dialect.SPANNER.name().equals(getAsString(Feature.dialect)) + && (getToken(1).kind == K_UNIQUE || isKeywordAhead("NULL_FILTERED")) }) + [ token= { index.setType(token.image); } ] + [ LOOKAHEAD({ isKeywordAhead("NULL_FILTERED") }) token= + { statement.setNullFiltered(true); } ] + | + [ parameter=CreateParameter() { index.setType(parameter.get(0)); } ] + ) + { return index; } } -ExpressionList SimpleExpressionList(): +CreateIndex CreateIndex(): { - ExpressionList columns; - ExpressionList expressions = new ExpressionList(); - Expression expr; + CreateIndex createIndex = new CreateIndex(); + Table table = null; + List colNames; + List includeColumns; + Boolean nullsDistinct = null; + List storageParameters = null; + String tableSpace = null; + Expression where = null; + String indexType; + Index index; + ObjectNames name; } { - expr=SimpleExpression() { expressions.add(expr); } + index=CreateIndexModifiers(createIndex) + + + [ LOOKAHEAD({ getToken(1).kind == K_CONCURRENTLY }) + { createIndex.setConcurrently(true); } ] + [ LOOKAHEAD(2) { createIndex.setUsingIfNotExists(true);} ] + [ LOOKAHEAD({ getToken(1).kind != K_ON && getToken(1).kind != K_USING }) + name=RelObjectNames() { index.setName(name.getNames()); } ] ( - LOOKAHEAD(2, {!interrupted} ) "," + LOOKAHEAD(3)( + [ LOOKAHEAD(2) { createIndex.setOnly(true); } ] table=Table() + [ indexType=UsingIndexType() { index.setUsing(indexType); } ] + ) + | ( - LOOKAHEAD( 7 ) expr=LambdaExpression() - | - expr=SimpleExpression() + [ indexType=UsingIndexType() { + index.setUsing(indexType); + createIndex.setIndexTypeBeforeOn(true); + } + ] + [ LOOKAHEAD(2) { createIndex.setOnly(true); } ] table=Table() ) + ) + colNames = IndexColumnsWithParamsList() + [ includeColumns=ColumnsNamesList() + { createIndex.setIncludeColumns(includeColumns); } + ] + [ LOOKAHEAD(3) [ { nullsDistinct = false; } ] { - expressions.add(expr); + if (nullsDistinct == null) { + nullsDistinct = true; + } + createIndex.setNullsDistinct(nullsDistinct); } - )* + ] + [ LOOKAHEAD(2) storageParameters=PostgreSqlIndexOptions() + { createIndex.setStorageParameters(storageParameters); } ] + [ LOOKAHEAD(2) tableSpace=RelObjectName() + { createIndex.setTableSpace(tableSpace); } ] + [ LOOKAHEAD(2) where=Expression() { createIndex.setWhere(where); } ] + ( LOOKAHEAD(2, { getToken(1).kind != K_LOCK || getToken(2).kind != K_TABLE }) + CreateIndexTailOption(createIndex, index) )* { - return expressions; + index.setColumns(colNames); + createIndex.setIndex(index); + createIndex.setTable(table); + return createIndex; } } - -ExpressionList ColumnList(): +/** + * Index options shared with table key definitions are kept on the index, ALGORITHM and LOCK on + * the statement. Other trailing tokens remain opaque index options. + */ +void CreateIndexTailOption(CreateIndex statement, Index index): { - ExpressionList expressions = new ExpressionList(); - Column expr; + AlgorithmOrLockOption algorithmOrLock; + IndexSpecOption option; + List parameter; } { - expr=Column() { expressions.add(expr); } - ( LOOKAHEAD(2) "," expr=Column() { expressions.add(expr); } )* - { - return expressions; - } + ( + LOOKAHEAD({ getToken(1).kind == K_ALGORITHM || getToken(1).kind == K_LOCK }) + algorithmOrLock=IndexAlgorithmOrLockOption() + { statement.addAlgorithmOrLockOptions(algorithmOrLock); } + | + LOOKAHEAD(IndexSpecOption(), { !(getToken(1).kind == K_USING && getToken(2).kind == K_INDEX) }) + option=IndexSpecOption() { index.getIndexOptions().addSpecOptions(option); } + | + parameter=CreateParameter() + { index.getIndexOptions().addSpecOptions(IndexSpecOption.raw(parameter)); } + ) } -ParenthesedExpressionList ParenthesedColumnList(): +// CREATE INDEX and DROP INDEX share MySQL's algorithm_option / lock_option syntax. +AlgorithmOrLockOption IndexAlgorithmOrLockOption(): { - ExpressionList expressions; + AlgorithmOrLockOption.Kind kind; + boolean useEquals = false; + Token value; } { - "(" expressions = ColumnList() ")" - { - return new ParenthesedExpressionList(expressions); - } + ( { kind = AlgorithmOrLockOption.Kind.ALGORITHM; } + | { kind = AlgorithmOrLockOption.Kind.LOCK; } ) + [ "=" { useEquals = true; } ] + // COPY is one of MySQL's ALGORITHM values and also starts the COPY statement + ( value= | value= | value= | value= + | value= ) + { return new AlgorithmOrLockOption(kind, value.image, useEquals); } } -ExpressionList ComplexExpressionList(): -{ - ExpressionList columns; - ExpressionList expressions = new ExpressionList(); - Expression expr; -} -{ +List ColumnDefinitionParameter(): { + Token tk = null; + List parameter = new ArrayList(); +} { ( - LOOKAHEAD(2) expr=OracleNamedFunctionParameter() - | - LOOKAHEAD(2) expr=PostgresNamedFunctionParameter() + parameter=CreateParameter() | - expr=Expression() + ( + tk= + | tk= + | tk= + | tk= + | tk= + ) + { parameter.add(tk.image); } ) + { return parameter; } +} + +ColumnDefinition ColumnDefinition(): { + ColumnDefinition coldef; + String columnName; + ColDataType colDataType; + List columnOptions = new ArrayList(); + ColumnOption option; +} { + columnName=RelObjectName() + colDataType=ColDataType() + ( LOOKAHEAD(2) option=ColumnDefinitionOption() { + columnOptions.add(option); + } )* { - expressions.add(expr); + coldef = new ColumnDefinition(); + coldef.setColumnName(columnName); + coldef.setColDataType(colDataType); + if (!columnOptions.isEmpty()) + coldef.setColumnOptions(columnOptions); + return coldef; } +} +ColumnOption ColumnDefinitionOption(): { + Token tk; + List foreignOptions; + List parameter; + ForeignKeyReference reference; + ColumnOption option; + IdentityDefinition identity; + NamedConstraint constraint; + String constraintName = null; + Expression defaultExpression; + GeneratedColumnDefinition generated; + ObjectNames collationNames; + String collation; + String compression; + Token engineAttribute; + boolean useEquals = false; +} { ( - LOOKAHEAD(2, {!interrupted}) "," - ( - LOOKAHEAD(2) expr=OracleNamedFunctionParameter() - | - LOOKAHEAD(2) expr=PostgresNamedFunctionParameter() - | - LOOKAHEAD(7) expr=LambdaExpression() - | - expr=Expression() - ) { expressions.add(expr); } - )* - - { - return expressions; - } + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) && isKeywordAhead("OPTIONS") }) + foreignOptions=ForeignDataOptions(false) { option = ColumnOption.foreignOptions(foreignOptions); } + | + LOOKAHEAD({ isKeywordAhead("GENERATED") + && (getToken(4).kind == K_IDENTITY || getToken(5).kind == K_IDENTITY) }) + identity=IdentityDefinition() { option = ColumnOption.identity(identity); } + | + LOOKAHEAD({ (isKeywordAhead("GENERATED") && getToken(2).kind == K_ALWAYS + && getToken(3).kind == K_AS && "(".equals(getToken(4).image)) + || (getToken(1).kind == K_AS && "(".equals(getToken(2).image)) }) + generated=GeneratedColumnDefinition() { option = ColumnOption.generated(generated); } + | + LOOKAHEAD([ RelObjectName() ] , + { Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && (getToken(1).kind == K_CONSTRAINT || getToken(3).kind == K_NO) }) + constraint=PostgreSqlColumnNotNullConstraint() { option = ColumnOption.constraint(constraint); } + | + LOOKAHEAD( ) { option = ColumnOption.nullability(false); } + | + LOOKAHEAD(1, ) { option = ColumnOption.nullability(true); } + | + LOOKAHEAD(1, ) { option = ColumnOption.visibility(true); } + | + LOOKAHEAD(1, ) { option = ColumnOption.visibility(false); } + | + LOOKAHEAD(1, ) { option = ColumnOption.autoIncrement(); } + | + LOOKAHEAD(1, ) ( tk= | tk= | tk= ) + { option = ColumnOption.storage(accessEnum(ColumnOption.Storage.class, tk.image)); } + | + LOOKAHEAD({ isKeywordAhead("COMPRESSION") }) ContextualKeyword("COMPRESSION") compression=RelObjectName() + { option = ColumnOption.compression(compression); } + | + // MySQL column attributes. Other values keep the raw fallback, e.g. Percona's COMPRESSED. + LOOKAHEAD({ isKeywordAhead("COLUMN_FORMAT") && (getToken(2).kind == K_DEFAULT + || "FIXED".equalsIgnoreCase(getToken(2).image) + || "DYNAMIC".equalsIgnoreCase(getToken(2).image)) }) + ContextualKeyword("COLUMN_FORMAT") ( tk= | tk= ) + { option = ColumnOption.columnFormat(accessEnum(ColumnOption.ColumnFormat.class, tk.image)); } + | + LOOKAHEAD({ isKeywordAhead("SRID") && getToken(2).kind == S_LONG }) + ContextualKeyword("SRID") tk= { option = ColumnOption.srid(Long.parseLong(tk.image)); } + | + LOOKAHEAD({ isMySqlEngineAttributeAhead() && (getToken(2).kind == S_CHAR_LITERAL + || "=".equals(getToken(2).image) && getToken(3).kind == S_CHAR_LITERAL) }) + engineAttribute= [ "=" { useEquals = true; } ] tk= + { + option = "ENGINE_ATTRIBUTE".equalsIgnoreCase(engineAttribute.image) + ? ColumnOption.engineAttribute(stringValue(tk)) + : ColumnOption.secondaryEngineAttribute(stringValue(tk)); + option.setUseEquals(useEquals); + } + | + LOOKAHEAD(1, ) + ( tk= { collation = tk.image; } + | collationNames=RelObjectNames() { collation = String.join(".", collationNames.getNames()); } ) + { option = ColumnOption.collate(collation); } + | + LOOKAHEAD(1, ) tk= + { option = ColumnOption.comment(stringValue(tk)); } + | + LOOKAHEAD( ) defaultExpression=Expression() + { option = ColumnOption.onUpdate(defaultExpression); } + | + // Keep SQL Server's inline FOREIGN KEY prefix together so its KEY is not a primary key. + LOOKAHEAD( ) { option = ColumnOption.raw("FOREIGN", "KEY"); } + | + LOOKAHEAD([ RelObjectName() ] ( | | )) + constraint=ColumnKeyConstraint() { option = ColumnOption.constraint(constraint); } + | + LOOKAHEAD([ RelObjectName() ] ) + [ constraintName=RelObjectName() ] + constraint=CheckConstraintSpec(constraintName) + { option = ColumnOption.constraint(constraint); } + | + LOOKAHEAD({ isKeywordAhead("SERIAL") + && getToken(2).kind == K_DEFAULT && getToken(3).kind == K_VALUE }) + tk= + { option = ColumnOption.serialDefaultValue(); } + | + LOOKAHEAD([ RelObjectName() ] ) + [ constraintName=RelObjectName() ] + reference=ForeignKeyReferenceSpec(true) + { reference.setConstraintName(constraintName); option = ColumnOption.reference(reference); } + | + LOOKAHEAD( ) + { option = ColumnOption.raw("DROP", "DEFAULT"); } + | + LOOKAHEAD( ) + { option = ColumnOption.raw("DEFAULT", "ON", "NULL"); } + | + LOOKAHEAD(1, ) { option = ColumnOption.raw("DEFAULT"); } + // Db2 permits an omitted default value. Keep these implicit defaults raw. + [ LOOKAHEAD(2, { !(getToken(1).kind == K_NOT && getToken(2).kind == K_NULL) }) + defaultExpression=ColumnDefaultValue() { option = ColumnOption.defaultValue(defaultExpression); } ] + | + parameter=ColumnDefinitionParameter() + { option = ColumnOption.raw(parameter); } + ) + { return option; } } -// @Todo: Refactor this with proper SQL:2016 functions according to https://manticore-projects.com/SQL2016Parser/syntax.html#character-value-function -// substring(expr1 from expr2) -// substring(expr1 from expr2 for expr3) -// trim(expr1 from expr2) <-- Superceded by TrimFunction() below -// position(expr1 in expr2) -// overlay(expr1 placing expr2 from expr3) -// overlay(expr1 placing expr2 from expr3 for expr4) -// expr1 has already been consumed -NamedExpressionList NamedExpressionListExprFirst(): +/** + * An explicit column DEFAULT value. MySQL's DEFAULT literal and PostgreSQL's DEFAULT b_expr exclude + * COLLATE, so the value ends before a COLLATE at its top level, which is the column's own + * attribute. A COLLATE inside parentheses, function arguments or CASE stays in the expression. + */ +Expression ColumnDefaultValue(): { - NamedExpressionList retval = new NamedExpressionList(); - List expressions = new ArrayList(); - List names = new ArrayList(); - Expression expr1 = null; - Expression expr2 = null; - Expression expr3 = null; - Expression expr4 = null; - Token tk2 = null; - Token tk3 = null; - Token tk4 = null; + Expression expression; } { - expr1=SimpleExpression() - (tk2=|tk2=|tk2=) - { - names.add(""); - expressions.add(expr1); - names.add(tk2.image); - } - ( - expr2=SimpleExpression() { expressions.add(expr2);} - ( - (tk3=|tk3=) - expr3=SimpleExpression() {names.add(tk3.image); expressions.add(expr3);} - ( - (tk4=) - expr4=SimpleExpression() {names.add(tk4.image); expressions.add(expr4);} - )? - )? - ) - - { - retval.setNames(names); - retval.setExpressions(expressions); - return retval; + { columnDefaultStarts.push(getToken(1)); } + try { + expression=Expression() + } finally { + columnDefaultStarts.pop(); } + { return expression; } } -Expression ComparisonItem() : +GeneratedColumnDefinition GeneratedColumnDefinition(): { - Expression retval = null; + Expression expression; + boolean generatedAlways = false; + GeneratedColumnDefinition.Storage storage = null; + GeneratedColumnDefinition result; } { - ( - LOOKAHEAD(6, { getToken(1).kind == K_ANY || getToken(1).kind == K_SOME || getToken(1).kind == K_ALL }) retval=AnyComparisonExpression() - | - LOOKAHEAD({ getToken(1).kind == K_ROW }) retval=RowConstructor() - | - retval=SimpleExpression() - ) + [ LOOKAHEAD({ isKeywordAhead("GENERATED") }) ContextualKeyword("GENERATED") + { generatedAlways = true; } ] + expression=ColumnGenerationExpression() + [ LOOKAHEAD({ getToken(1).kind == K_STORED || isKeywordAhead("VIRTUAL") }) + ( { storage = GeneratedColumnDefinition.Storage.STORED; } + | ContextualKeyword("VIRTUAL") { storage = GeneratedColumnDefinition.Storage.VIRTUAL; } ) ] + { + result = new GeneratedColumnDefinition(expression); + result.setGeneratedAlways(generatedAlways); + result.setStorage(storage); + return result; + } +} - { - return retval; - } +/** Shared expression body for generated declarations and ALTER SET EXPRESSION. */ +Expression ColumnGenerationExpression(): +{ Expression expression; } +{ + "(" expression=Expression() ")" + { return expression; } } -Expression AnyComparisonExpression() : +/** + * Keeps a column key's name and attributes on the same node in CREATE and ALTER, including MySQL's + * UNIQUE KEY and KEY (PRIMARY KEY) spellings. + */ +KeyConstraint ColumnKeyConstraint(): { - AnyType anyType; - Select select; + KeyConstraint constraint = new KeyConstraint(); + String name; + Boolean nullsDistinct = null; } { + [ name=RelObjectName() { constraint.setName(name); } ] ( - { anyType = AnyType.ANY; } + { constraint.setType("PRIMARY KEY"); } | - { anyType = AnyType.SOME; } + // MySQL accepts KEY as a column-level synonym for PRIMARY KEY; keep its spelling. + { constraint.setType("KEY"); constraint.setKind(ConstraintKind.PRIMARY_KEY); } | - { anyType = AnyType.ALL; } + { constraint.setType("UNIQUE"); } + [ LOOKAHEAD(1) { constraint.setType("UNIQUE KEY"); } ] + [ [ { nullsDistinct = false; } ] { + constraint.setNullsDistinct(nullsDistinct == null ? true : nullsDistinct); + } ] ) - select = ParenthesedSelect() + PostgreSqlIndexStorageOptions(constraint) + PostgreSqlConstraintAttributes(constraint) { - return new AnyComparisonExpression(anyType, select); + requireDdlSyntax(constraint.getConstraintAttributes() == null + || !constraint.getConstraintAttributes().isNotValid(), + "NOT VALID requires a table constraint"); + return constraint; } } -Expression SimpleExpression(): +IdentityDefinition IdentityDefinition(): { - Expression retval = null; - UserVariable user = null; - Token operation = null; + Token token; + IdentityDefinition.GenerationMode mode; + IdentityDefinition identity; + List parameters = new ArrayList(); + Sequence.Parameter parameter; + Sequence sequenceName; } { - [ LOOKAHEAD( 5, { getToken(1).kind == S_AT_IDENTIFIER } ) user = UserVariable() ( operation = "=" | operation = ":=" ) ] - retval=PrimaryExpression() - { retval = prattArithRest(retval, 5); } - { - if (user != null) { - VariableAssignment assignment = new VariableAssignment(); - assignment.setVariable(user); - assignment.setOperation(operation.image); - assignment.setExpression(retval); - return assignment; - } else - return retval; + token= { + requireDdlSyntax("GENERATED".equalsIgnoreCase(token.image), "Expected GENERATED"); } + ( { mode = IdentityDefinition.GenerationMode.ALWAYS; } + | { mode = IdentityDefinition.GenerationMode.BY_DEFAULT; } ) + { identity = new IdentityDefinition(mode); } + [ LOOKAHEAD(1) "(" + ( + ( + LOOKAHEAD( ) + sequenceName=Sequence() + { parameter = new Sequence.Parameter(Sequence.ParameterType.SEQUENCE_NAME) + .withSequenceName(sequenceName); } + | parameter=SequenceParameter() + ) + { parameters.add(parameter); } + )+ + ")" { + identity.setParameters(parameters); + } ] + { return identity; } } -/** Multiplicative/bitwise-shift — higher precedence */ -Expression MulChain(): +StringValue MySqlAccountNamePart(): +{ + Token token = null; + String name = null; + StringValue value = null; +} +{ + ( + token= { value = stringValue(token); } + | + name=RelObjectName() + { + // a double-quoted or backquoted part keeps its quote character separately from its + // value, like a string literal; new StringValue(name) would drop the double quotes + value = name.length() > 1 && (name.charAt(0) == '"' || name.charAt(0) == '`') + && name.charAt(name.length() - 1) == name.charAt(0) + ? new StringValue().withValue(name.substring(1, name.length() - 1)) + .setQuoteStr(name.substring(0, 1)) + : new StringValue().withValue(name).setQuoteStr(""); + linkToLastToken(value); + } + ) + { return value; } +} + +/** + * A MySQL account name, user[@host], shared by CREATE USER, DEFINER and the account statements. + * Each part is unquoted or quoted with ', " or `. The lexer reads an unquoted user@host, and the + * user@ in front of a quoted host, as one identifier, and @host after a quoted user as one + * at-identifier, so those spellings are split here. + */ +UserAccount MySqlAccountName(): { - Expression retval; - Expression right; + UserAccount account = new UserAccount(); + StringValue part; + Token token; + int at; } { - retval=PrimaryExpression() ( - LOOKAHEAD(2) - ( - "*" right=PrimaryExpression() {{ Multiplication r = new Multiplication(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - | - right=PrimaryExpression() {{ Division r = new Division(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - | - "%" right=PrimaryExpression() {{ Modulo r = new Modulo(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + // user@ followed by a quoted host + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0 + && getToken(1).image.indexOf('@') == getToken(1).image.length() - 1 }) + token= + { account.setUser(new StringValue().withValue(token.image.substring(0, token.image.length() - 1)) + .setQuoteStr("")); } + part=MySqlAccountNamePart() { account.setHost(part); } + | + // unquoted user@host + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0 + && getToken(1).image.indexOf('@') == getToken(1).image.lastIndexOf('@') }) + token= + { + at = token.image.indexOf('@'); + account.setUser(new StringValue().withValue(token.image.substring(0, at)).setQuoteStr("")); + account.setHost(new StringValue().withValue(token.image.substring(at + 1)).setQuoteStr("")); + } + | + part=MySqlAccountNamePart() { account.setUser(part); } + [ + // CURRENT_USER() names the current user like CURRENT_USER; a quoted part has quotes in its image + LOOKAHEAD({ "CURRENT_USER".equalsIgnoreCase(getToken(0).image) + && "(".equals(getToken(1).image) && ")".equals(getToken(2).image) }) + "(" ")" { account.setCurrentUserParentheses(true); } | - right=PrimaryExpression() {{ net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + part=MySqlAccountNamePart() { account.setHost(part); } | - right=PrimaryExpression() {{ IntegerDivision r = new IntegerDivision(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - ) - )* - { return retval; } + // a quoted user followed by an unquoted @host + LOOKAHEAD({ getToken(1).kind == S_AT_IDENTIFIER && !getToken(1).image.startsWith("@@") }) + token= + { account.setHost(new StringValue().withValue(token.image.substring(1)).setQuoteStr("")); } + ] + ) + { return account; } } -/** Additive/concat/bitwise — lower precedence */ -Expression AddChain(): +List MySqlAccountNameList(): { - Expression retval; - Expression right; + List accounts = new ArrayList(); + UserAccount account; } { - retval=MulChain() - ( - LOOKAHEAD(2) - ( - "+" right=MulChain() {{ Addition r = new Addition(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - | - "-" right=MulChain() {{ Subtraction r = new Subtraction(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - | - right=MulChain() {{ Concat r = new Concat(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - | - LOOKAHEAD(2) right=MulChain() {{ Concat r = new Concat(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - | - right=MulChain() {{ BitwiseOr r = new BitwiseOr(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - | - "&" right=MulChain() {{ BitwiseAnd r = new BitwiseAnd(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - | - right=MulChain() {{ BitwiseLeftShift r = new BitwiseLeftShift(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - | - right=MulChain() {{ BitwiseRightShift r = new BitwiseRightShift(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} - ) - )* - { return retval; } + account=MySqlAccountName() { accounts.add(account); } + ( "," account=MySqlAccountName() { accounts.add(account); } )* + { return accounts; } } -Expression ArrayExpression(Expression obj): { - Expression expr; - Expression idxExpr = null; - Expression startExpr = null; - Expression stopExpr = null; -} { - "[" - [LOOKAHEAD(3) idxExpr = SimpleExpression()] +/** + * Roles of SET ROLE, SET DEFAULT ROLE, ALTER USER ... DEFAULT ROLE and GRANT ... AS user WITH ROLE. + * Only SET ROLE and WITH ROLE (setRole) accept DEFAULT and ALL EXCEPT. + */ +RoleSelection MySqlRoleSelection(boolean setRole): +{ + RoleSelection selection; + List roles; +} +{ + ( + LOOKAHEAD({ getToken(1).kind == K_DEFAULT }) + { selection = new RoleSelection(RoleSelection.Kind.DEFAULT); } + | + LOOKAHEAD({ getToken(1).kind == K_NONE }) + { selection = new RoleSelection(RoleSelection.Kind.NONE); } + | + LOOKAHEAD({ getToken(1).kind == K_ALL }) + { selection = new RoleSelection(RoleSelection.Kind.ALL); } [ - (":" { startExpr=idxExpr; idxExpr=null; }) - [stopExpr = SimpleExpression()] + LOOKAHEAD({ getToken(1).kind == K_EXCEPT }) roles=MySqlAccountNameList() + { selection.setKind(RoleSelection.Kind.ALL_EXCEPT); selection.setRoles(requireRoleNames(roles)); } ] - "]" - { expr = new ArrayExpression(obj, idxExpr, startExpr, stopExpr); } - ( - LOOKAHEAD(2) "[" - [LOOKAHEAD(3) idxExpr = SimpleExpression()] - [ - (":" { startExpr=idxExpr; idxExpr=null; }) - [stopExpr = SimpleExpression()] - ] - "]" - { expr = new ArrayExpression(expr, idxExpr, startExpr, stopExpr); } - )* - { return expr; } + | + roles=MySqlAccountNameList() + { selection = new RoleSelection(RoleSelection.Kind.ROLES).withRoles(requireRoleNames(roles)); } + ) + { + requireAccessSyntax(setRole || selection.getKind() != RoleSelection.Kind.DEFAULT + && selection.getKind() != RoleSelection.Kind.ALL_EXCEPT, + "DEFAULT and ALL EXCEPT require SET ROLE"); + return selection; + } } -Expression PrimaryExpression() #PrimaryExpression: +/** + * IDENTIFIED BY or IDENTIFIED WITH. The plugin may be quoted like an account name part, and the + * hash after AS may be a hexadecimal literal, as SHOW CREATE USER prints both. + */ +UserAuthentication MySqlUserAuthentication(): { - Expression retval = null; - Expression expression = null; - CastExpression castExpr = null; - TimezoneExpression timezoneExpr = null; - Expression timezoneRightExpr = null; - Token token = null; - Token sign = null; - String tmp = ""; - ColDataType type = null; - boolean not = false; - boolean exclamationMarkNot = false; - boolean dateExpressionAllowed = true; - ExpressionList list; - - final List> jsonIdents = new ArrayList>(); + UserAuthentication authentication = new UserAuthentication(); + StringValue plugin = null; + Token credential = null; } { - [ { not=true; } | "!" { not=true; exclamationMarkNot=true; } ] - [sign="+" | sign="-" | sign="~"] + ( - { retval = new NullValue(); } - - | LOOKAHEAD({!interrupted && isCaseExpressionAhead()}) retval=CaseWhenExpression() - - | LOOKAHEAD(2, {!interrupted}) retval=CharacterPrimary() - - | LOOKAHEAD({!interrupted && isImplicitCastAhead()}) retval=ImplicitCast() - - | retval = JdbcParameter() - - | LOOKAHEAD(2) retval =JdbcNamedParameter() - - | LOOKAHEAD(3) retval=UserVariable() - - | LOOKAHEAD(2, {!interrupted}) retval=NumericBind() - - | LOOKAHEAD(4 , {!interrupted}) retval=ExtractExpression() - - | retval=XMLSerializeExpr() - - | LOOKAHEAD(3, { !interrupted}) retval = JsonFunction() - - | LOOKAHEAD(3, { !interrupted}) retval = JsonAggregateFunction() - - | LOOKAHEAD(3, { !interrupted}) retval = FullTextSearch() - - | LOOKAHEAD(2, {!interrupted}) retval= CastExpression() - - | LOOKAHEAD({ !interrupted && isFunctionAhead() }) - retval = Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ] - - | LOOKAHEAD(2) retval = DateUnitExpression() - - | LOOKAHEAD(2, {!interrupted}) retval = IntervalExpression() { dateExpressionAllowed = false; } - - | token= { retval = new DoubleValue(token.image); } - - | token= { retval = new LongValue(token.image); } - - | token= { retval = new HexValue(token.image); } - - | LOOKAHEAD(3) retval=AllColumns(true) - - | LOOKAHEAD({ !interrupted && isAllTableColumnsAhead() }) - retval=AllTableColumns(true) - - // See issue #2207 - // there is a huge! performance deterioration from this production - //| LOOKAHEAD(FunctionAllColumns()) retval=FunctionAllColumns() - - // support timestamp expressions - | LOOKAHEAD(2, {!interrupted}) (token= | token=) { retval = new TimeKeyExpression(token.image); } - - | LOOKAHEAD(2, {!interrupted}) retval=DateTimeLiteralExpression() - - | LOOKAHEAD(3 , {!interrupted}) retval=StructType() - - | LOOKAHEAD(3, {!interrupted}) [ "<" type=ColDataType() ">" ] retval=ArrayConstructor(true) { if (type!=null) ((ArrayConstructor) retval).setDataType(type); } - - | LOOKAHEAD(3, {!interrupted}) retval=ArrayConstructor(false) - - | LOOKAHEAD(2, {!interrupted}) retval = NextValExpression() - - | retval=ConnectByRootOperator() - - | retval=ConnectByPriorOperator() - - | LOOKAHEAD(2, { !interrupted && getToken(1).kind == K_KEY && getToken(2).kind != OPENING_BRACKET }) retval=KeyExpression() - - | LOOKAHEAD(2, {!interrupted}) { retval = new AllValue(); } - - | LOOKAHEAD(2, {!interrupted}) retval=Column() - [ - LOOKAHEAD( "(" "+" ")" ("," | ")") ) - "(" "+" ")" { ((Column) retval).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); } - ] - - | LOOKAHEAD(2, {!interrupted}) (token= | token=) { retval = new BooleanValue(token.image); } - - | token= { retval = new StringValue(token.image); linkAST(retval,jjtThis); } - - | "{d" token= "}" { retval = new DateValue(token.image); } - - | "{t" token= "}" { retval = new TimeValue(token.image); } - - | "{ts" token= "}" { retval = new TimestampValue(token.image); } - - | LOOKAHEAD({ getAsBoolean(Feature.allowUnparenthesizedSubSelects) && !interrupted - && (getToken(1).kind == K_SELECT || getToken(1).kind == K_WITH || getToken(1).kind == K_VALUES - || (getToken(1).kind == OPENING_BRACKET && (getToken(2).kind == K_SELECT || getToken(2).kind == K_WITH || getToken(2).kind == K_VALUES))) }) retval=Select() - - | LOOKAHEAD({ !getAsBoolean(Feature.allowUnparenthesizedSubSelects) && !interrupted && isParenthesedSelectAhead() }) retval=ParenthesedSelect() - - | LOOKAHEAD({ !interrupted && isNestedSetOperationAhead() }) retval=ParenthesedSelect() - - | + ( - list=ParenthesedExpressionList() - // Mutli-Variable Lambda Expression, e. g. - // SELECT map_filter(my_column, (k,v) -> v.my_inner_column = 'some_value') - // First-arg form (issue #2195): array_map((x,y,z) -> x + y, ...) - ( - LOOKAHEAD(2) "->" - retval = Expression() - { - retval = LambdaExpression.from(list, retval); - } - | - { - if (list.size() == 1) { - retval = new ParenthesedExpressionList( (Expression) list.getExpressions().get(0)); - } else { - retval = list; - } - } - ) - - // RowGet Expressions - ( LOOKAHEAD(2) "." tmp=RelObjectName() { retval = new RowGetExpression(retval, tmp); } )* - ) - ) - - [ - LOOKAHEAD(2) (token= | token= | token=) { retval = new CollateExpression(retval, token.image); } - ] - - [ - LOOKAHEAD(2, { dateExpressionAllowed } ) retval = IntervalExpressionWithoutInterval(retval) - ] - - [ LOOKAHEAD(2) retval = ArrayExpression(retval) ] - - ( LOOKAHEAD(2) "::" type=ColDataType() { - castExpr = new CastExpression(); - castExpr.setUseCastKeyword(false); - castExpr.setLeftExpression(retval); - castExpr.setColDataType(type); - retval=castExpr; - } - )* - - // Check for JSON operands - [ - LOOKAHEAD(2) ( - LOOKAHEAD(2) ( - token="->" - | - token=":" - | - token="->>" - | - token="#>" - | - token="#>>" - ) - - ( - LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted }) expression=Expression() - | - expression=SimpleExpression() - ) - { - jsonIdents.add(new AbstractMap.SimpleEntry(expression, token.image )); + credential= { + authentication.setMode(UserAuthentication.Mode.BY_PASSWORD); + authentication.setCredential(stringValue(credential)); } - )+ - retval = JsonExpression(retval, jsonIdents) - ] - - ( LOOKAHEAD(2) timezoneRightExpr=PrimaryExpression() { - if (timezoneExpr == null) - timezoneExpr = new TimezoneExpression(); - - timezoneExpr.addTimezoneExpression(timezoneRightExpr); - } - )* - - { - if (timezoneExpr != null && !timezoneExpr.getTimezoneExpressions().isEmpty()) { - timezoneExpr.setLeftExpression(retval); - retval=timezoneExpr; - } - if (sign != null) { - retval = new SignedExpression(sign.image.charAt(0), retval); - } - if (not) { - retval = new NotExpression(retval, exclamationMarkNot); + | + { + authentication.setMode(UserAuthentication.Mode.BY_RANDOM_PASSWORD); + } + ) + | + plugin=MySqlAccountNamePart() { + authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN); + authentication.setPluginName(plugin); } - linkAST(retval, jjtThis); - return retval; - } + [ + + ( + credential= { + authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN_BY_PASSWORD); + authentication.setCredential(stringValue(credential)); + } + | + { + authentication.setMode( + UserAuthentication.Mode.WITH_PLUGIN_BY_RANDOM_PASSWORD); + } + ) + | + { authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN_AS_STRING); } + ( + credential= { authentication.setCredential(stringValue(credential)); } + | + credential= + { authentication.setCredentialValue(new HexValue(credential.image)); } + ) + ] + ) + { return authentication; } } -/* https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/img_text/hierarchical_query_clause.html - - { CONNECT BY [ NOCYCLE ] condition [ START WITH condition ] - | START WITH condition CONNECT BY [ NOCYCLE ] condition - } - */ - -ConnectByRootOperator ConnectByRootOperator() #ConnectByRootOperator: { - Expression expression; +UserAccount MySqlUserAccount(): +{ + UserAccount account; + UserAuthentication authentication = null; } { - expression = Expression() - { - return new ConnectByRootOperator(expression); - } + account=MySqlAccountName() + [ authentication=MySqlUserAuthentication() { account.setAuthentication(authentication); } ] + { return account; } } -ConnectByPriorOperator ConnectByPriorOperator() #ConnectByPriorOperator: { - Expression expression; +CreateUser CreateUser(): +{ + CreateUser createUser = new CreateUser(); + UserAccount account = null; } { - expression = Expression() - { - return new ConnectByPriorOperator(expression); - } + + [ LOOKAHEAD(3) { createUser.setIfNotExists(true); } ] + account=MySqlUserAccount() { createUser.addAccounts(account); } + ( "," account=MySqlUserAccount() { createUser.addAccounts(account); } )* + MySqlUserOptions(createUser.getOptions(), true) + { return createUser; } } -KeyExpression KeyExpression() #KeyExpression: { - Expression expression; +/** + * MySQL account options shared by CREATE USER and ALTER USER, in the fixed clause order. Only + * CREATE USER lists DEFAULT ROLE here; ALTER USER uses its own single-account form. + */ +void MySqlUserOptions(UserOptions options, boolean create): +{ + RoleSelection roles; + TlsOption tls; + ResourceLimit limit; + PasswordLockOption option; + Token value; } { - expression = Expression() - { - return new KeyExpression(expression); - } + [ + LOOKAHEAD({ getToken(1).kind == K_DEFAULT && "ROLE".equalsIgnoreCase(getToken(2).image) }) + ContextualKeyword("ROLE") roles=MySqlRoleSelection(false) + { + requireAccessSyntax(create, + "ALTER USER accepts DEFAULT ROLE only for a single account"); + requireAccessSyntax(roles.getKind() == RoleSelection.Kind.ROLES, + "CREATE USER DEFAULT ROLE requires role names"); + options.setDefaultRoles(roles); + } + ] + [ + LOOKAHEAD({ isKeywordAhead("REQUIRE") }) ContextualKeyword("REQUIRE") + ( + LOOKAHEAD({ getToken(1).kind == K_NONE }) + { options.addTlsOptions(new TlsOption(TlsOption.Kind.NONE)); } + | + tls=MySqlTlsOption() + { + requireAccessSyntax(!tls.isAndKeyword(), "REQUIRE cannot start with AND"); + options.addTlsOptions(tls); + } + ( + LOOKAHEAD({ isMySqlTlsOptionAhead() }) + tls=MySqlTlsOption() { options.addTlsOptions(tls); } + )* + ) + ] + [ + LOOKAHEAD({ getToken(1).kind == K_WITH && isMySqlResourceLimitAhead(2) }) + ( + LOOKAHEAD({ isMySqlResourceLimitAhead(1) }) + limit=MySqlResourceLimit() { options.addResourceLimits(limit); } + )+ + ] + ( + LOOKAHEAD({ isMySqlPasswordLockOptionAhead(1) }) + option=MySqlPasswordLockOption() { options.addPasswordLockOptions(option); } + )* + [ + LOOKAHEAD({ getToken(1).kind == K_COMMENT && getToken(2).kind == S_CHAR_LITERAL }) + value= { options.setComment(stringValue(value)); } + | + LOOKAHEAD({ isKeywordAhead("ATTRIBUTE") && getToken(2).kind == S_CHAR_LITERAL }) + ContextualKeyword("ATTRIBUTE") value= + { options.setAttribute(stringValue(value)); } + ] } - -NextValExpression NextValExpression() : { - ObjectNames data = null; - Token token; +/** One REQUIRE option, preceded by the optional AND that separates the options. */ +TlsOption MySqlTlsOption(): +{ + TlsOption option = new TlsOption(); + Token word; + Token value; } { - token= data = RelObjectNames() + [ { option.setAndKeyword(true); } ] + word= { option.setKind(accessEnum(TlsOption.Kind.class, word.image)); } + [ + LOOKAHEAD({ getToken(1).kind == S_CHAR_LITERAL }) + value= { option.setValue(stringValue(value)); } + ] { - return new NextValExpression(data.getNames(), token.image); + requireAccessSyntax(option.getKind().hasValue() == (option.getValue() != null), + "Invalid REQUIRE option " + word.image); + return option; } } -JdbcNamedParameter JdbcNamedParameter() : { - JdbcNamedParameter parameter = new JdbcNamedParameter(); - String name; - String namePart; +ResourceLimit MySqlResourceLimit(): +{ + Token word; + Token value; } { - (":" | "&" { parameter.setParameterCharacter("&"); } ) - name=IdentifierChain() + word= value= { - parameter.setName(name); - return parameter; + return new ResourceLimit(accessEnum(ResourceLimit.Kind.class, word.image), + Long.parseLong(value.image)); } } -OracleNamedFunctionParameter OracleNamedFunctionParameter() : { - Token token = null; - String name = null; - Expression expression; +PasswordLockOption MySqlPasswordLockOption(): +{ + PasswordLockOption option = null; + Token value; } { - ( name=RelObjectNameExt() | token= ) - - expression=Expression() - { - return new OracleNamedFunctionParameter(name != null ? name : token.image, expression); - } + ( + LOOKAHEAD({ isKeywordAhead("ACCOUNT") }) ContextualKeyword("ACCOUNT") + ( + { option = new PasswordLockOption(PasswordLockOption.Kind.ACCOUNT_LOCK); } + | + ContextualKeyword("UNLOCK") + { option = new PasswordLockOption(PasswordLockOption.Kind.ACCOUNT_UNLOCK); } + ) + | + LOOKAHEAD({ isKeywordAhead("FAILED_LOGIN_ATTEMPTS") }) + ContextualKeyword("FAILED_LOGIN_ATTEMPTS") value= + { + option = new PasswordLockOption(PasswordLockOption.Kind.FAILED_LOGIN_ATTEMPTS) + .withValue(Long.valueOf(value.image)); + } + | + LOOKAHEAD({ isKeywordAhead("PASSWORD_LOCK_TIME") }) ContextualKeyword("PASSWORD_LOCK_TIME") + { option = new PasswordLockOption(PasswordLockOption.Kind.PASSWORD_LOCK_TIME); } + ( + value= { option.setValue(Long.valueOf(value.image)); } + | + { option.setSetting(PasswordLockOption.Setting.UNBOUNDED); } + ) + | + + ( + LOOKAHEAD({ isKeywordAhead("EXPIRE") }) ContextualKeyword("EXPIRE") + { option = new PasswordLockOption(PasswordLockOption.Kind.PASSWORD_EXPIRE); } + [ + { option.setSetting(PasswordLockOption.Setting.DEFAULT); } + | + { option.setSetting(PasswordLockOption.Setting.NEVER); } + | + value= MySqlDayUnit() + { option.setValue(Long.valueOf(value.image)); } + ] + | + + { option = new PasswordLockOption(PasswordLockOption.Kind.PASSWORD_HISTORY); } + ( + { option.setSetting(PasswordLockOption.Setting.DEFAULT); } + | + value= { option.setValue(Long.valueOf(value.image)); } + ) + | + LOOKAHEAD({ isKeywordAhead("REUSE") }) ContextualKeyword("REUSE") + { option = new PasswordLockOption(PasswordLockOption.Kind.PASSWORD_REUSE_INTERVAL); } + ( + { option.setSetting(PasswordLockOption.Setting.DEFAULT); } + | + value= MySqlDayUnit() { option.setValue(Long.valueOf(value.image)); } + ) + | + ContextualKeyword("REQUIRE") + { option = new PasswordLockOption(PasswordLockOption.Kind.PASSWORD_REQUIRE_CURRENT); } + [ + { option.setSetting(PasswordLockOption.Setting.DEFAULT); } + | + LOOKAHEAD({ isKeywordAhead("OPTIONAL") }) ContextualKeyword("OPTIONAL") + { option.setSetting(PasswordLockOption.Setting.OPTIONAL); } + ] + ) + ) + { return option; } } -PostgresNamedFunctionParameter PostgresNamedFunctionParameter() : { - Token token = null; - String name = null; - Expression expression; -} +void MySqlDayUnit(): +{ Token unit; } { - ( name=RelObjectNameExt() | token= ) - - expression=Expression() - { - return new PostgresNamedFunctionParameter(name != null ? name : token.image, expression); - } + unit= + { requireAccessSyntax("DAY".equalsIgnoreCase(unit.image), "Expected DAY"); } } -UserVariable UserVariable() : { - Token tk; - String varName; +/** + * ALTER USER authentication: an IDENTIFIED clause, whose password assignment may add REPLACE and + * RETAIN CURRENT PASSWORD, or DISCARD OLD PASSWORD. + */ +UserAuthentication MySqlAlterUserAuthentication(): +{ + UserAuthentication authentication; + Token value; } { - tk = varName=IdentifierChain2(tk.image) - { - return new UserVariable(varName); - } + ( + authentication=MySqlUserAuthentication() + [ + LOOKAHEAD({ getToken(1).kind == K_REPLACE && getToken(2).kind == S_CHAR_LITERAL }) + value= + { authentication.setCurrentCredential(stringValue(value)); } + ] + [ + LOOKAHEAD({ isKeywordAhead("RETAIN") }) ContextualKeyword("RETAIN") + { authentication.setRetainCurrentPassword(true); } + ] + { + requireAccessSyntax(authentication.getMode().isPasswordAssignment() + || authentication.getCurrentCredential() == null + && !authentication.isRetainCurrentPassword(), + "REPLACE and RETAIN CURRENT PASSWORD require a password assignment"); + } + | + ContextualKeyword("OLD") + { + authentication = new UserAuthentication() + .withMode(UserAuthentication.Mode.DISCARD_OLD_PASSWORD); + } + ) + { return authentication; } } -NumericBind NumericBind() : { - NumericBind var = new NumericBind(); - Token token; +UserAccount MySqlAlterUserAccount(): +{ + UserAccount account; + UserAuthentication authentication; } { - ":" token= - { - var.setBindId(Integer.valueOf(token.image)); - return var; - } + account=MySqlAccountName() + [ + LOOKAHEAD({ getToken(1).kind == K_IDENTIFIED || getToken(1).kind == K_DISCARD }) + authentication=MySqlAlterUserAuthentication() { account.setAuthentication(authentication); } + ] + { return account; } } -DateTimeLiteralExpression DateTimeLiteralExpression() : { - DateTimeLiteralExpression expr = new DateTimeLiteralExpression(); - Token t; +/** + * MySQL ALTER USER: accounts with authentication changes and the options shared with CREATE USER, + * USER() with a password change, or one account with DEFAULT ROLE. + */ +AlterUser AlterUser(): +{ + AlterUser alterUser = new AlterUser(); + UserAccount account; + UserAuthentication authentication; + RoleSelection roles; } { - t= - { - expr.setType(DateTimeLiteralExpression.DateTime.from(t.image)); - } - - ( t= | t= ) - { - expr.setValue(t.image); - return expr; - } + + [ LOOKAHEAD(2) { alterUser.setIfExists(true); } ] + ( + LOOKAHEAD({ getToken(1).kind == K_USER && "(".equals(getToken(2).image) }) + "(" ")" authentication=MySqlAlterUserAuthentication() + { + requireAccessSyntax(authentication.getMode() == UserAuthentication.Mode.BY_PASSWORD + || authentication.getMode() == UserAuthentication.Mode.BY_RANDOM_PASSWORD + || authentication.getMode() == UserAuthentication.Mode.DISCARD_OLD_PASSWORD, + "USER() accepts IDENTIFIED BY or DISCARD OLD PASSWORD"); + alterUser.withUserFunction(authentication); + } + | + account=MySqlAlterUserAccount() { alterUser.addAccounts(account); } + ( + LOOKAHEAD({ getToken(1).kind == K_DEFAULT && "ROLE".equalsIgnoreCase(getToken(2).image) }) + ContextualKeyword("ROLE") roles=MySqlRoleSelection(false) + { + requireAccessSyntax(account.getAuthentication() == null, + "DEFAULT ROLE cannot follow an authentication change"); + alterUser.getOptions().setDefaultRoles(roles); + } + | + ( "," account=MySqlAlterUserAccount() { alterUser.addAccounts(account); } )* + MySqlUserOptions(alterUser.getOptions(), false) + ) + ) + { return alterUser; } } -DateUnitExpression DateUnitExpression() : { - Token t; +/** MySQL CREATE ROLE [IF NOT EXISTS] role [, role] ... with account-style role names. */ +CreateRole MySqlCreateRole(): +{ + CreateRole role = new CreateRole(); + List roles; } { - t= { return new DateUnitExpression( t.image ); } + ContextualKeyword("ROLE") + [ LOOKAHEAD(3) { role.setIfNotExists(true); } ] + roles=MySqlAccountNameList() { role.setRoles(requireRoleNames(roles)); } + { return role; } } -RangeExpression RangeExpression(Expression startExpression): +RenameUser RenameUser(): { - Expression endExpression; + RenameUser renameUser = new RenameUser(); + UserAccount from; + UserAccount to; } { - ":" endExpression = Expression() - { - return new RangeExpression(startExpression, endExpression); - } + + from=MySqlAccountName() to=MySqlAccountName() { renameUser.addRename(from, to); } + ( + "," from=MySqlAccountName() to=MySqlAccountName() + { renameUser.addRename(from, to); } + )* + { return renameUser; } } -ArrayConstructor ArrayConstructor(boolean arrayKeyword) : { - ExpressionList expList = new ExpressionList(); - ArrayConstructor array = new ArrayConstructor(expList, arrayKeyword); - Expression exp; -} { - "[" - [ - ( - LOOKAHEAD(3) exp = Expression() [ exp=RangeExpression(exp) ] - | - exp = ArrayConstructor(false) - ) { expList.add(exp); } +SetRoleStatement SetRoleStatement(): +{ RoleSelection roles; } +{ + ContextualKeyword("ROLE") roles=MySqlRoleSelection(true) + { return new SetRoleStatement(roles); } +} - ( - "," - ( - LOOKAHEAD(3) exp = Expression() [ exp=RangeExpression(exp) ] - | - exp = ArrayConstructor(false) - ){ expList.add(exp); } - )* +/** MySQL SET DEFAULT ROLE ... TO accounts, or MariaDB's form with an optional FOR account. */ +SetDefaultRoleStatement SetDefaultRoleStatement(): +{ + SetDefaultRoleStatement statement = new SetDefaultRoleStatement(); + RoleSelection roles; + List accounts; +} +{ + ContextualKeyword("ROLE") roles=MySqlRoleSelection(false) + { statement.setRoles(roles); } + [ + ( | { statement.setForKeyword(true); } ) + // MySQL reads these accounts as a role list + accounts=MySqlAccountNameList() { statement.setAccounts(requireRoleNames(accounts)); } ] - "]" - { return array; } + { return statement; } } -List> StructParameters(): +SetPasswordStatement SetPasswordStatement(): { - String parameterName = ""; - ColDataType parameterType = null; - AbstractMap.SimpleEntry parameter = null; - List> parameters = new ArrayList>(); + SetPasswordStatement statement = new SetPasswordStatement(); + UserAccount account; + Expression password; + Token value; } { - [ LOOKAHEAD(2) parameterName = RelObjectName() ] - parameterType = ColDataType() - { - parameters.add( new AbstractMap.SimpleEntry(parameterName, parameterType) ); - } - + + [ account=MySqlAccountName() { statement.setAccount(account); } ] ( - "," { parameterName=""; } + "=" password=Expression() { statement.setPassword(password); } + | + { statement.setRandom(true); } + ) + [ + LOOKAHEAD({ getToken(1).kind == K_REPLACE && getToken(2).kind == S_CHAR_LITERAL }) + value= { statement.setCurrentCredential(stringValue(value)); } + ] + [ + LOOKAHEAD({ isKeywordAhead("RETAIN") }) ContextualKeyword("RETAIN") + { statement.setRetainCurrentPassword(true); } + ] + { return statement; } +} - [ LOOKAHEAD(2) parameterName = RelObjectName() ] - parameterType = ColDataType() - { - parameters.add( new AbstractMap.SimpleEntry(parameterName, parameterType) ); +EventSchedule MySqlEventSchedule(): +{ + EventSchedule schedule = new EventSchedule(); + Expression expression = null; + Token unitToken = null; + String unit = null; +} +{ + + ( + expression=Expression() { + schedule.setType(EventSchedule.Type.AT); + schedule.setExecuteAt(expression); + } + | + expression=MySqlEventIntervalValue() { + schedule.setType(EventSchedule.Type.EVERY); + schedule.setInterval(expression); } - )* - - { - return parameters; - } + ( LOOKAHEAD({ getToken(1).kind == K_DATE_LITERAL }) + unitToken= { schedule.setIntervalUnit(unitToken.image); } + | unit=RelObjectName() { schedule.setIntervalUnit(unit); } ) + [ expression=Expression() { schedule.setStarts(expression); } ] + [ expression=Expression() { schedule.setEnds(expression); } ] + ) + { return schedule; } } -StructType StructType() #StruckType: +Expression MySqlEventIntervalValue(): { - StructType.Dialect dialect = StructType.Dialect.BIG_QUERY; - Token tk1; - String keyword = ""; - List> parameters = null; - List> arguments = null; - String id = null; + Token value = null; Expression expression = null; - StructType type; } { ( - LOOKAHEAD(4) ( - tk1= { keyword = tk1.image; } - "<" parameters = StructParameters() ">" - "(" arguments = SelectItemsList() ")" - ) + value= { expression = new LongValue(value.image); } | - ( - tk1= { keyword = tk1.image; } - "(" arguments = SelectItemsList() ")" - ) + value= { expression = new DoubleValue(value.image); } | - ( - { arguments= new ArrayList>(); dialect = StructType.Dialect.DUCKDB;} - ( id = RelObjectNameExt() | tk1= { id = tk1.image; } ) - expression = Expression() { arguments.add( new SelectItem( expression, id) ); } + value= { expression = stringValue(value); } + ) + { return expression; } +} - ( - "," - ( id = RelObjectNameExt() | tk1= { id = tk1.image; } ) - expression = Expression() { arguments.add( new SelectItem( expression, id) ); } - )* - +Boolean MySqlEventCompletion(): +{ + boolean preserve = true; +} +{ + [ { preserve = false; } ] + LOOKAHEAD({ "PRESERVE".equalsIgnoreCase(getToken(1).image) }) + + { return preserve; } +} - ( - LOOKAHEAD(2) "::" "(" parameters = StructParameters() ")" - )* - ) +EventStatus MySqlEventStatus(): +{ + EventStatus status = null; +} +{ + ( + { status = EventStatus.ENABLE; } + | + { status = EventStatus.DISABLE; } + [ LOOKAHEAD({ getToken(1).kind == K_ON && (getToken(2).kind == K_SLAVE + || "REPLICA".equalsIgnoreCase(getToken(2).image)) }) + ( + { status = EventStatus.DISABLE_ON_SLAVE; } + | + ContextualKeyword("REPLICA") { status = EventStatus.DISABLE_ON_REPLICA; } + ) ] ) - { - type = new StructType(dialect, keyword, parameters, arguments); - linkAST(type,jjtThis); - return type; - } + { return status; } } -JsonExpression JsonExpression(Expression expr, List> idents) : { - JsonExpression result = new JsonExpression(expr, idents); - Expression expression; - Token token=null; - ColDataType type = null; - CastExpression castExpr = null; +CreateEvent CreateEvent(): +{ + CreateEvent createEvent = new CreateEvent(); + Table event = null; + EventSchedule schedule = null; + Boolean preserve = null; + EventStatus status = null; + Token comment = null; + Statement body = null; + MySqlDefiner definer = null; } { + [ LOOKAHEAD({ isKeywordAhead("DEFINER") }) + definer=MySqlRoutineDefiner() { createEvent.setDefiner(definer); } ] + + [ LOOKAHEAD(3) { createEvent.setIfNotExists(true); } ] + event=Table() { createEvent.setEvent(event); } + schedule=MySqlEventSchedule() { createEvent.setSchedule(schedule); } + [ LOOKAHEAD(2) preserve=MySqlEventCompletion() { + createEvent.setOnCompletionPreserve(preserve); + } ] + [ LOOKAHEAD({ getToken(1).kind == K_ENABLE || getToken(1).kind == K_DISABLE }) + status=MySqlEventStatus() { createEvent.setStatus(status); } ] + [ LOOKAHEAD({ getToken(1).kind == K_COMMENT }) comment= { + createEvent.setComment(stringValue(comment)); + } ] + body=MySqlTriggerBody() { createEvent.setBody(body); } + { return createEvent; } +} + +AlterEvent AlterEvent(): +{ + AlterEvent alterEvent = new AlterEvent(); + Table event = null; + Table renameTo = null; + EventSchedule schedule = null; + Boolean preserve = null; + EventStatus status = null; + Token comment = null; + Statement body = null; + MySqlDefiner definer = null; +} +{ + [ LOOKAHEAD({ isKeywordAhead("DEFINER") }) + definer=MySqlRoutineDefiner() { alterEvent.setDefiner(definer); } ] + event=Table() { alterEvent.setEvent(event); } + [ LOOKAHEAD({ getToken(1).kind == K_ON && getToken(2).kind == K_SCHEDULE }) + schedule=MySqlEventSchedule() { alterEvent.setSchedule(schedule); } ] + [ LOOKAHEAD({ getToken(1).kind == K_ON && getToken(2).kind == K_COMPLETION }) + preserve=MySqlEventCompletion() { alterEvent.setOnCompletionPreserve(preserve); } ] + [ LOOKAHEAD({ getToken(1).kind == K_RENAME }) + renameTo=Table() { alterEvent.setRenameTo(renameTo); } ] + [ LOOKAHEAD({ getToken(1).kind == K_ENABLE || getToken(1).kind == K_DISABLE }) + status=MySqlEventStatus() { alterEvent.setStatus(status); } ] + [ LOOKAHEAD({ getToken(1).kind == K_COMMENT }) comment= { + alterEvent.setComment(stringValue(comment)); + } ] + [ LOOKAHEAD({ getToken(1).kind == K_DO }) + body=MySqlTriggerBody() { alterEvent.setBody(body); } ] + { return alterEvent; } +} - // chaining JSON Expressions, e.g. - // '{"obj":{"field": "value"}}'::JSON -> 'obj'::TEXT ->> 'field'::TEXT +/** Account spelling is shared, including CURRENT_USER and CURRENT_USER(). */ +void MySqlDefiner(MySqlDefiner definer): +{ + UserAccount account; + String name; +} +{ + ContextualKeyword("DEFINER") "=" ( - LOOKAHEAD(2, {!interrupted} ) ( - LOOKAHEAD(2) ( - "::" type=ColDataType() - { - castExpr = new CastExpression(); - castExpr.setUseCastKeyword(false); - castExpr.setLeftExpression(result); - castExpr.setColDataType(type); - expr=castExpr; - } - ) - )+ - { - result = new JsonExpression(expr); - } + LOOKAHEAD({ isKeywordAhead("CURRENT_USER") }) name=RelObjectName() + { definer.setUser(linkToLastToken(new StringValue(name).setQuoteStr(""))); } + [ "(" ")" { definer.setCurrentUserParentheses(true); } ] + | + account=MySqlAccountName() { definer.setUser(account.getUser()); definer.setHost(account.getHost()); } + ) +} - ( - LOOKAHEAD(2) ( - token="->" - | - token=":" - | - token="->>" - | - token="#>" - | - token="#>>" - ) +TriggerDefiner MySqlTriggerDefiner(): +{ TriggerDefiner definer = new TriggerDefiner(); } +{ MySqlDefiner(definer) { return definer; } } - ( - LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted}) expression=Expression() - | - expression=SimpleExpression() - ) - { - result.addIdent( expression, token.image ); - } - )* - )* +MySqlDefiner MySqlRoutineDefiner(): +{ MySqlDefiner definer = new MySqlDefiner(); } +{ MySqlDefiner(definer) { return definer; } } - { - result.setExpression(expr); - return result; - } +/** MySQL fixes the prefix order: ALGORITHM, DEFINER, SQL SECURITY. */ +MySqlViewOptions MySqlViewPrefix(): +{ + MySqlViewOptions options = new MySqlViewOptions(); + MySqlDefiner definer; + Token token; + boolean present = false; +} +{ + [ "=" ( token= | token= ) { + requireDdlSyntax("MERGE".equalsIgnoreCase(token.image) || "TEMPTABLE".equalsIgnoreCase(token.image) + || "UNDEFINED".equalsIgnoreCase(token.image), "Expected MERGE, TEMPTABLE, or UNDEFINED"); + options.setAlgorithm(MySqlViewOptions.Algorithm.valueOf(token.image.toUpperCase(Locale.ROOT))); + present = true; + } ] + [ LOOKAHEAD({ isKeywordAhead("DEFINER") }) { definer = new MySqlDefiner(); } + MySqlDefiner(definer) { options.setDefiner(definer); present = true; } ] + [ LOOKAHEAD({ isKeywordAhead("SQL") }) ContextualKeyword("SQL") token= { + requireDdlSyntax("DEFINER".equalsIgnoreCase(token.image) || "INVOKER".equalsIgnoreCase(token.image), + "Expected DEFINER or INVOKER"); + options.setSecurity(MySqlViewOptions.Security.valueOf(token.image.toUpperCase(Locale.ROOT))); + present = true; + } ] + { return present ? options : null; } } -JsonKeyValuePair JsonKeyValuePair(boolean isFirstEntry) : { - Token keyToken; - - boolean usingKeyKeyword = false; - boolean usingFormatJason = false; - String encoding = null; - boolean isWildcard = false; +CreateTrigger.Timing MySqlTriggerTiming(): +{ + CreateTrigger.Timing timing = null; +} +{ + ( + { timing = CreateTrigger.Timing.BEFORE; } + | + LOOKAHEAD({ "AFTER".equalsIgnoreCase(getToken(1).image) }) + { timing = CreateTrigger.Timing.AFTER; } + ) + { return timing; } +} - Object key = null; - Expression expression = null; - JsonFunctionExpression functionExpression; +CreateTrigger.Order MySqlTriggerOrder(): +{ + CreateTrigger.Order order = null; + Token token = null; +} +{ + LOOKAHEAD({ "FOLLOWS".equalsIgnoreCase(getToken(1).image) + || "PRECEDES".equalsIgnoreCase(getToken(1).image) }) + token= { + order = "FOLLOWS".equalsIgnoreCase(token.image) + ? CreateTrigger.Order.FOLLOWS : CreateTrigger.Order.PRECEDES; + } + { return order; } +} - JsonKeyValuePairSeparator kvSeparator = JsonKeyValuePairSeparator.NOT_USED; +/** + * MySQL trigger or event statement. BEGIN ... END with SQL statements only remains a Block. Other + * compound statements, such as those with IF or DECLARE, keep their tokens like routine bodies and + * use the same boundary tracking. The semicolon after END is left to separate statements. + */ +Statement MySqlTriggerBody(): +{ + Statement body; + List tokens; + Token first = getToken(1); } { ( - // lookahead because key is a valid column name - LOOKAHEAD(2) ( - { usingKeyKeyword = true; } - ( - keyToken = { key = keyToken.image; } | - key = Column() - ) - ) - | - LOOKAHEAD(16) key = AllTableColumns(false) { isWildcard = true; } - | - LOOKAHEAD(2) key = AllColumns(false) { isWildcard = true; } - | - LOOKAHEAD(2) key = Column() + LOOKAHEAD({ isStatementBlockAhead() }) body=BeginEndBlock() | - LOOKAHEAD({getAsBoolean(Feature.allowExpressionAsJsonObjectKey)}) key = Expression() + LOOKAHEAD({ isMySqlCompoundStatementAhead() }) tokens=captureCompoundStatement() + { body = linked(new UnsupportedStatement(tokens), first); } | - keyToken = { key = keyToken.image; } + body=SingleStatement() ) + { return body; } +} - // Optional Separator + Value - Is not allowed with * or t1.* - [ LOOKAHEAD(1, { !isWildcard } ) - ( - { kvSeparator = JsonKeyValuePairSeparator.VALUE; } - | - { kvSeparator = JsonKeyValuePairSeparator.COLON; } - | - LOOKAHEAD({getAsBoolean(Feature.allowCommaAsKeyValueSeparator)}) { kvSeparator = JsonKeyValuePairSeparator.COMMA; } - ) - expression = Expression() - ] - - // Optional: FORMAT JSON [ ENCODING ... ] - Is not allowed with * or t1.* - [ - LOOKAHEAD(1, { !isWildcard } ) { usingFormatJason = true; } - [ encoding = JsonEncoding() ] - ] - { - final JsonKeyValuePair keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, kvSeparator ); - keyValuePair.setUsingFormatJson( usingFormatJason ); - keyValuePair.setEncoding(encoding); - return keyValuePair; - } +/** + * Captures a compound statement through its END and optional end label. Qualified names are + * joined, and a semicolon or label colon is attached to the preceding token. + */ +JAVACODE +List captureCompoundStatement() { + List tokens = new LinkedList(); + RoutineBodyBoundary boundary = RoutineBodyBoundary.forCompoundStatement(); + boolean afterDot = false; + Token tok; + while (true) { + tok = getToken(1); + if (boundary.endsAt(tok)) { + break; + } + int last = tokens.size() - 1; + if (last >= 0 && (afterDot || ".".equals(tok.image) || ":".equals(tok.image) + || tok.kind == ST_SEMICOLON)) { + tokens.set(last, tokens.get(last) + tok.image); + } else { + tokens.add(tok.image); + } + afterDot = ".".equals(tok.image); + boundary.accept(tok, getToken(2)); + getNextToken(); + } + return tokens; } -JsonFunction JsonObjectBody() : { - JsonFunction result = new JsonFunction(JsonFunctionType.OBJECT); +CreateTrigger CreateTrigger(boolean orReplace): +{ + CreateTrigger createTrigger = new CreateTrigger(); + TriggerDefiner definer = null; + Table trigger = null; + CreateTrigger.Timing timing = null; + Token event = null; + Table table = null; + CreateTrigger.Order order = null; + Table otherTrigger = null; + Statement body = null; +} +{ + [ LOOKAHEAD({ "DEFINER".equalsIgnoreCase(getToken(1).image) }) + definer=MySqlTriggerDefiner() { createTrigger.setDefiner(definer); } ] + + [ LOOKAHEAD(2) { createTrigger.setIfNotExists(true); } ] + { + requireDdlSyntax(!orReplace || !createTrigger.isIfNotExists(), + "OR REPLACE and IF NOT EXISTS cannot be combined"); + createTrigger.setOrReplace(orReplace); + } + trigger=Table() { createTrigger.setTrigger(trigger); } + timing=MySqlTriggerTiming() { createTrigger.setTiming(timing); } + ( event= { createTrigger.setEvent(CreateTrigger.Event.INSERT); } + | event= { createTrigger.setEvent(CreateTrigger.Event.UPDATE); } + | event= { createTrigger.setEvent(CreateTrigger.Event.DELETE); } ) + table=Table() { createTrigger.setTable(table); } + + LOOKAHEAD({ "EACH".equalsIgnoreCase(getToken(1).image) }) + [ order=MySqlTriggerOrder() otherTrigger=Table() { + createTrigger.setOrder(order); + createTrigger.setOtherTrigger(otherTrigger); + } ] + body=MySqlTriggerBody() { createTrigger.setBody(body); } + { return createTrigger; } +} - JsonKeyValuePair keyValuePair; - ColDataType dataType; - String encoding; +CreateSchema CreateSchema(): +{ + Token tk = null; + Statement element = null; + CreateSchema schema = new CreateSchema(); + List schemaPath = null; + DatabaseOption databaseOption = null; } { - ( "(" - ( - // First Element - LOOKAHEAD(2) keyValuePair = JsonKeyValuePair(true) { result.add(keyValuePair);} + + [ LOOKAHEAD(2) { schema.setIfNotExists(true); } ] + [ LOOKAHEAD(1) + ( tk= | tk=) { schema.setSchemaName(tk.image); } - // Next Elements - ( - keyValuePair = JsonKeyValuePair(false) { result.add(keyValuePair); } - )* + ( + "." { schema.setCatalogName(tk.image); } + ( tk= | tk=) { schema.setSchemaName(tk.image); } )? - [ - ( { result.setOnNullType( JsonAggregateOnNullType.NULL ); } ) - | - ( { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } ) - ] - [ { result.setStrict(true); } ] - [ - ( { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); } ) - | - ( { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); } ) - ] - [ - dataType = ColDataType() { result.setReturningType(dataType); } - [ - { result.setReturningFormatJson(true); } - [ encoding = JsonEncoding() { result.setReturningEncoding(encoding); } ] - ] - ] - ")" ) + ] + + ( + LOOKAHEAD({ isMySqlDatabaseOptionAhead() }) + databaseOption=MySqlDatabaseOption() { schema.addDatabaseOptions(databaseOption); } + )* + + [ + (tk= | tk=) { schema.setAuthorization(tk.image); } + ] + + [schemaPath=PathSpecification() { schema.setSchemaPath(schemaPath); }] + + // Elements keep their names as written; unqualified names refer to the new schema. + ( + LOOKAHEAD({ getToken(1).kind == K_CREATE || getToken(1).kind == K_GRANT }) + element=SchemaElement() { schema.addStatement(element); } + )* { - return result; + requireDdlSyntax(!schema.hasIfNotExists() || schema.getStatements().isEmpty(), + "CREATE SCHEMA IF NOT EXISTS cannot include schema elements"); + return schema; } } -JsonFunction JsonArrayBody() : { - JsonFunction result = new JsonFunction(JsonFunctionType.ARRAY); - - Expression expression = null; - JsonFunctionExpression functionExpression; - ColDataType dataType; - String encoding; +/** + * A statement nested in CREATE SCHEMA. Only the object kinds that PostgreSQL accepts as schema + * elements are allowed: tables, views, indexes, sequences, triggers and GRANT. + */ +Statement SchemaElement(): +{ + Statement element; + Token first = getToken(1); } { - ( "(" + { schemaElementDepth++; } + try { ( - LOOKAHEAD(2) ( - { result.setOnNullType( JsonAggregateOnNullType.NULL ); } - ) + element=Create() + { + requireDdlSyntax(element instanceof CreateTable || element instanceof CreateIndex + || element instanceof CreateSequence || element instanceof CreateTrigger + || element instanceof CreateView && !((CreateView) element).isMaterialized(), + "A schema element must be CREATE TABLE, VIEW, INDEX, SEQUENCE, TRIGGER or GRANT"); + } | - expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } - - [ - LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } - [ LOOKAHEAD(2) encoding = JsonEncoding() { functionExpression.setEncoding(encoding); } ] - ] - ( - "," - expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } - [ - LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } - [ LOOKAHEAD(2) encoding = JsonEncoding() { functionExpression.setEncoding(encoding); } ] - ] - )* - )* - - [ - { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } - ] - [ - dataType = ColDataType() { result.setReturningType(dataType); } - [ - { result.setReturningFormatJson(true); } - [ encoding = JsonEncoding() { result.setReturningEncoding(encoding); } ] - ] - ] - ")" ) - { - return result; + element=Grant() + ) + } finally { + schemaElementDepth--; } + { linkStatement(element, first); return element; } } -void JsonKeyword(String expectedKeyword) : { +DatabaseOption MySqlDatabaseOption(): +{ + boolean useDefault = false; + boolean useEquals = false; + DatabaseOption.Kind kind = DatabaseOption.Kind.OTHER; + String name = null; + String value = null; + Token valueToken = null; +} +{ + [ { useDefault = true; } ] + ( + { + kind = DatabaseOption.Kind.CHARACTER_SET; + name = "CHARACTER SET"; + } + | + { + kind = DatabaseOption.Kind.CHARACTER_SET; + name = "CHAR SET"; + } + | + LOOKAHEAD({ isKeywordAhead("CHARSET") }) ContextualKeyword("CHARSET") { + kind = DatabaseOption.Kind.CHARACTER_SET; + name = "CHARSET"; + } + | + { + kind = DatabaseOption.Kind.COLLATE; + name = "COLLATE"; + } + | + { + kind = DatabaseOption.Kind.ENCRYPTION; + name = "ENCRYPTION"; + } + ) + [ "=" { useEquals = true; } ] + ( value=RelObjectName() | valueToken= { value = valueToken.image; } ) + { return new DatabaseOption(kind, name, value, useDefault, useEquals); } +} + +/** PostgreSQL option of CREATE DATABASE; ALTER DATABASE accepts the same option list. */ +DatabaseOption PostgreSqlDatabaseOption(): +{ Token token; + DatabaseOption.Kind kind; + String name; + String value; + boolean useEquals = false; } { - token = - { - if (!token.image.equalsIgnoreCase(expectedKeyword)) { - throw new ParseException( - "Expected keyword " + expectedKeyword + " but found " + token.image); + ( + LOOKAHEAD({ isKeywordAhead("CONNECTION") }) ContextualKeyword("CONNECTION") + { kind = DatabaseOption.Kind.CONNECTION_LIMIT; name = "CONNECTION LIMIT"; } + | + { kind = DatabaseOption.Kind.ENCODING; name = "ENCODING"; } + | + { kind = DatabaseOption.Kind.TABLESPACE; name = "TABLESPACE"; } + | + LOOKAHEAD({ isPostgreSqlDatabaseOptionAhead(1) }) token= { + name = token.image.toUpperCase(Locale.ROOT); + kind = DatabaseOption.Kind.valueOf(name); } - } + ) + [ "=" { useEquals = true; } ] + value=PostgreSqlDatabaseOptionValue() + { return new DatabaseOption(kind, name, value, false, useEquals); } } -String JsonEncoding() : { +/** A name, string, signed number, boolean or DEFAULT, as written. */ +String PostgreSqlDatabaseOptionValue(): +{ Token token; + String value; + String sign = ""; } { - token = - { - if (token.image.equalsIgnoreCase("UTF8")) { - return "UTF8"; - } else if (token.image.equalsIgnoreCase("UTF16")) { - return "UTF16"; - } else if (token.image.equalsIgnoreCase("UTF32")) { - return "UTF32"; + ( + LOOKAHEAD(1) token= { value = token.image; } + | + LOOKAHEAD(1) token= { value = token.image; } + | + ( token= | token= | token= ) { value = token.image; } + | + [ "+" { sign = "+"; } | "-" { sign = "-"; } ] + ( token= | token= ) { value = sign + token.image; } + | + value=RelObjectName() + ) + { return value; } +} + +/** + * MySQL ALTER DATABASE/SCHEMA options and the PostgreSQL ALTER DATABASE forms. Options reuse the + * CREATE DATABASE productions; READ ONLY is specific to MySQL's ALTER. Only MySQL options may + * follow an omitted name. + */ +AlterDatabase AlterDatabase(): +{ + AlterDatabase result = new AlterDatabase(); + String name; + DatabaseOption option; + ConfigurationParameterChange change; +} +{ + ( + { result.setUseSchemaKeyword(true); } + ( + LOOKAHEAD({ isAlterDatabaseNameOmitted() }) MySqlAlterDatabaseOptions(result) + | + name=RelObjectName() { result.setDatabaseName(name); } MySqlAlterDatabaseOptions(result) + ) + | + + ( + LOOKAHEAD({ isAlterDatabaseNameOmitted() }) MySqlAlterDatabaseOptions(result) + | + name=RelObjectName() { result.setDatabaseName(name); } + ( + name=RelObjectName() + { result.setAction(AlterDatabase.Action.RENAME); result.setNewName(name); } + | + LOOKAHEAD({ isKeywordAhead("OWNER") && getToken(2).kind == K_TO }) + ContextualKeyword("OWNER") name=RelObjectName() + { result.setAction(AlterDatabase.Action.OWNER); result.setOwner(name); } + | + LOOKAHEAD({ getToken(1).kind == K_SET && getToken(2).kind == K_TABLESPACE + && getToken(3).kind != K_TO && getToken(3).kind != K_FROM && !"=".equals(getToken(3).image) }) + name=RelObjectName() + { result.setAction(AlterDatabase.Action.SET_TABLESPACE); result.setTablespace(name); } + | + ContextualKeyword("COLLATION") + { result.setAction(AlterDatabase.Action.REFRESH_COLLATION_VERSION); } + | + change=ConfigurationParameterChange() + { result.setAction(AlterDatabase.Action.CONFIGURATION_PARAMETER); result.setParameterChange(change); } + | + [ { result.setUseWith(true); } ] + ( + ( + LOOKAHEAD({ isPostgreSqlDatabaseOptionAhead(1) }) option=PostgreSqlDatabaseOption() + | + option=MySqlAlterDatabaseOption() + ) { result.getOptions().add(option); } + )+ + ) + ) + ) + { return result; } +} + +void MySqlAlterDatabaseOptions(AlterDatabase result): +{ DatabaseOption option; } +{ + ( option=MySqlAlterDatabaseOption() { result.getOptions().add(option); } )+ +} + +/** MySQL ALTER DATABASE option: a CREATE DATABASE option or READ ONLY. */ +DatabaseOption MySqlAlterDatabaseOption(): +{ + DatabaseOption option; + Token value; + boolean equals = false; +} +{ + ( + LOOKAHEAD({ isMySqlDatabaseOptionAhead() }) option=MySqlDatabaseOption() + | + + [ "=" { equals = true; } ] + ( value= | value= ) { + requireDdlSyntax(value.kind == K_DEFAULT || "0".equals(value.image) || "1".equals(value.image), + "READ ONLY requires DEFAULT, 0, or 1"); + option = new DatabaseOption(DatabaseOption.Kind.READ_ONLY, "READ ONLY", value.image, false, equals); } - throw new ParseException( - "Expected ENCODING value UTF8, UTF16 or UTF32 but found " + token.image); - } + ) + { return option; } } -JsonFunctionExpression JsonValueOrQueryInputExpression() : { - Expression expression; - JsonFunctionExpression functionExpression; - String encoding; +List PathSpecification(): +{ + Token tk; + List pathList = new ArrayList(); } { - expression = Expression() { functionExpression = new JsonFunctionExpression(expression); } - [ - { functionExpression.setUsingFormatJson(true); } - [ encoding = JsonEncoding() { functionExpression.setEncoding(encoding); } ] - ] + (tk=|tk=) { pathList.add(tk.image); } + ("," (tk=|tk=) { pathList.add(tk.image); })* { - return functionExpression; + return pathList; } } -JsonFunction.JsonOnResponseBehavior JsonValueOnResponseBehavior() : { - JsonFunction.JsonOnResponseBehavior behavior; - Expression expression; +/** Typed MySQL and PostgreSQL options; other option text is kept as one raw option. */ +CreateDatabase CreateDatabase(): +{ + CreateDatabase createDatabase = new CreateDatabase(); + String databaseName = null; + List databaseOptions = null; + DatabaseOption databaseOption = null; } { + + [ LOOKAHEAD(2) { createDatabase.setIfNotExists(true); } ] + databaseName = RelObjectName() { createDatabase.setDatabaseName(databaseName); } + [ LOOKAHEAD({ getToken(1).kind == K_WITH && isPostgreSqlDatabaseOptionAhead(2) }) + { createDatabase.setUseWith(true); } ] ( - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.ERROR); - } - | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.NULL); - } - | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY); - } - | - expression = Expression() - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.DEFAULT, expression); + LOOKAHEAD({ isMySqlDatabaseOptionAhead() || isPostgreSqlDatabaseOptionAhead(1) }) + ( + LOOKAHEAD({ isMySqlDatabaseOptionAhead() }) databaseOption=MySqlDatabaseOption() + | + databaseOption=PostgreSqlDatabaseOption() + ) { createDatabase.addOptions(databaseOption); } + )* + [ databaseOptions = captureRest() { + if (!databaseOptions.isEmpty()) { + createDatabase.addOptions(DatabaseOption.raw(databaseOptions)); } - ) + } ] { - return behavior; + return createDatabase; } } -JsonFunction.JsonOnResponseBehavior JsonQueryOnResponseBehavior() : { - JsonFunction.JsonOnResponseBehavior behavior = null; +/** Shares optional SQL Server constraint modifiers across CREATE and ALTER. */ +IndexOptions.Clustering SqlServerIndexClustering(): +{ Token token; + IndexOptions.Clustering clustering = null; +} +{ + [ LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + && (isKeywordAhead("CLUSTERED") || isKeywordAhead("NONCLUSTERED")) }) + token= { + clustering = IndexOptions.Clustering.valueOf(token.image.toUpperCase(Locale.ROOT)); + } + ] + { return clustering; } +} + +/** Parses PRIMARY, UNIQUE, plain, FULLTEXT, and SPATIAL indexes for CREATE and ALTER. */ +TableElement TableIndexSpec(boolean createContext): +{ + Token typeToken = null; + Token keywordToken = null; + String indexName = null; + String using = null; + List columns; + List indexOptions = new ArrayList(); + TableElement index; + Boolean nullsDistinct = null; + IndexOptions.Clustering clustering = null; } { ( - + typeToken= keywordToken= + clustering=SqlServerIndexClustering() + [ LOOKAHEAD({ clustering == null && getToken(1).kind != OPENING_BRACKET + && getToken(1).kind != K_USING }) indexName=RelObjectName() ] + // MySQL's index_type may also precede the key list: PRIMARY KEY USING BTREE (id) + [ LOOKAHEAD({ getToken(1).kind == K_USING && getToken(2).kind != K_INDEX + && getToken(2).kind != K_COLUMNS }) using=UsingIndexType() ] + columns=IndexKeyColumns(true) + TableIndexOptions(createContext, indexOptions) { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.ERROR); + index = new KeyConstraint() + .withIndexName(indexName) + .withType(typeToken.image + " " + keywordToken.image) + .withUsing(using).withColumns(columns) + .withSpecOptions(indexOptions); } | - + typeToken= + [ LOOKAHEAD(2) [ { nullsDistinct = false; } ] { + if (nullsDistinct == null) { nullsDistinct = true; } + } ] + [ LOOKAHEAD(2) (keywordToken= | keywordToken=) ] + clustering=SqlServerIndexClustering() + [ LOOKAHEAD({ clustering == null && getToken(1).kind != OPENING_BRACKET + && getToken(1).kind != K_USING }) indexName=RelObjectName() ] + [ using=UsingIndexType() ] + columns=IndexKeyColumns(true) + TableIndexOptions(createContext, indexOptions) { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.NULL); + // PostgreSQL and MySQL ALTER preserve named constraints separately from index names. + if (createContext || Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) + || Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { + index = new KeyConstraint() + .withIndexName(indexName) + .withType(typeToken.image + + (keywordToken != null ? " " + keywordToken.image : "")) + .withUsing(using).withColumns(columns).withSpecOptions(indexOptions); + } else { + index = new KeyConstraint().withType(typeToken.image) + .withIndexKeyword(keywordToken != null ? keywordToken.image : null) + .withIndexName(indexName).withUsing(using).withColumns(columns) + .withSpecOptions(indexOptions); + } } | - + typeToken= + [ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET + && getToken(1).kind != K_USING }) indexName=RelObjectName() ] + [ using=UsingIndexType() ] + columns=IndexColumnsWithParamsList() + TableIndexOptions(createContext, indexOptions) { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.TRUE); + index = new Index().withType(createContext ? typeToken.image : null) + .withIndexKeyword(typeToken.image).withKind(Index.Kind.INDEX) + .withName(indexName).withUsing(using).withColumns(columns) + .withSpecOptions(indexOptions); } | - + typeToken= + [ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET + && getToken(1).kind != K_USING }) indexName=RelObjectName() ] + [ using=UsingIndexType() ] + columns=IndexColumnsWithParamsList() + TableIndexOptions(createContext, indexOptions) { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.FALSE); + index = new Index().withType(createContext ? typeToken.image : null) + .withIndexKeyword(typeToken.image).withKind(Index.Kind.INDEX) + .withName(indexName).withUsing(using).withColumns(columns) + .withSpecOptions(indexOptions); } | - + (typeToken= | typeToken=) + [ LOOKAHEAD(2) (keywordToken= | keywordToken=) ] + [ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET }) indexName=RelObjectName() ] + columns=IndexColumnsWithParamsList() + TableIndexOptions(createContext, indexOptions) { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY); + index = new Index().withType(typeToken.image + + (createContext && keywordToken != null + ? " " + keywordToken.image : "")) + .withIndexKeyword(!createContext && keywordToken != null + ? keywordToken.image : null) + .withName(indexName).withColumns(columns).withSpecOptions(indexOptions); } - [ + ) + { ((IndexOptionSource) index).setNullsDistinct(nullsDistinct); + ((IndexOptionSource) index).setClustering(clustering); } + PostgreSqlConstraintOptions((IndexOptionSource) index) + { return index; } +} + +/** + * Index options after the key list, shared by CREATE TABLE and ALTER TABLE key definitions. + * CREATE TABLE also retains other accepted tokens as opaque options. + */ +void TableIndexOptions(boolean createContext, List options): +{ + IndexSpecOption option; + List parameter; +} +{ + ( + LOOKAHEAD({ createContext }) + ( LOOKAHEAD(2, { !(getToken(1).kind == K_WITH && getToken(2).kind == OPENING_BRACKET) + && !(getToken(1).kind == K_USING && getToken(2).kind == K_INDEX) + && getToken(1).kind != K_DEFERRABLE && !isKeywordAhead("INITIALLY") + && !(getToken(1).kind == K_NOT && (getToken(2).kind == K_DEFERRABLE + || "VALID".equalsIgnoreCase(getToken(2).image))) }) ( - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY); - } + LOOKAHEAD(IndexSpecOption()) option=IndexSpecOption() { options.add(option); } | - JsonKeyword("OBJECT") - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY_OBJECT); - } + parameter=CreateParameter() { options.add(IndexSpecOption.raw(parameter)); } ) - ] + )* + | + LOOKAHEAD({ !createContext }) + ( + LOOKAHEAD(IndexSpecOption(), { !(getToken(1).kind == K_USING && getToken(2).kind == K_INDEX) }) + option=IndexSpecOption() { options.add(option); } + )* ) +} + +void PostgreSqlConstraintOptions(IndexOptionSource index): +{ + List columns; +} +{ + [ columns=ColumnsNamesList() { index.setIncludeColumns(columns); } ] + PostgreSqlIndexStorageOptions(index) +} + +/** Column keys share storage/tablespace options, but do not allow INCLUDE. */ +void PostgreSqlIndexStorageOptions(IndexOptionSource index): +{ + List parameters; + String tableSpace; +} +{ + [ LOOKAHEAD(2) parameters=PostgreSqlIndexOptions() { index.setStorageParameters(parameters); } ] + [ LOOKAHEAD(3) tableSpace=RelObjectName() { + index.setTableSpace(tableSpace); + } ] +} + +void PostgreSqlConstraintAttributes(TableElement element): +{ + NamedConstraint constraint = element instanceof NamedConstraint ? (NamedConstraint) element : null; + ConstraintAttributes attributes; +} +{ + attributes=PostgreSqlConstraintAttributeList(constraint == null ? null : constraint.getConstraintAttributes()) + ( LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) }) + TypeDdlKeyword("INHERIT") { + requireDdlSyntax(constraint instanceof CheckConstraint && !((CheckConstraint) constraint).isNoInherit(), + "NO INHERIT requires a CHECK constraint and cannot be repeated"); + ((CheckConstraint) constraint).setNoInherit(true); + } + attributes=PostgreSqlConstraintAttributeList(attributes) + )* { - return behavior; + requireDdlSyntax(attributes == null || attributes.getEnforced() == null + || constraint instanceof CheckConstraint || constraint instanceof ForeignKeyConstraint, + "ENFORCED is supported only for CHECK and foreign key constraints"); + if (constraint != null) { + constraint.setConstraintAttributes(attributes); + } else if (attributes != null) { + // Retain permissive legacy index syntax without treating indexes as constraints. + StringBuilder suffix = new StringBuilder(); + attributes.appendTo(suffix); + ((Index) element).getTailParameters().add(suffix.toString().trim()); + } } } -JsonFunction.JsonOnResponseBehavior JsonExistsOnResponseBehavior() : { - JsonFunction.JsonOnResponseBehavior behavior = null; +/** Shared attribute parsing for table constraints and column REFERENCES clauses. */ +ConstraintAttributes PostgreSqlConstraintAttributeList(ConstraintAttributes attributes): +{ + boolean present = attributes != null; + boolean negative; + Boolean enforced; + Token token; } { - ( - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.TRUE); + { if (attributes == null) { attributes = new ConstraintAttributes(); } } + ( LOOKAHEAD({ isPostgreSqlConstraintAttributeAhead() }) ( + LOOKAHEAD(2) { negative = false; } + [ { negative = true; } ] { + requireDdlSyntax(attributes.getDeferrable() == null, "Duplicate DEFERRABLE clause"); + attributes.setDeferrable(!negative); present = true; } | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.FALSE); + LOOKAHEAD({ isKeywordAhead("INITIALLY") }) token= { + requireDdlSyntax(attributes.getInitially() == null, "Duplicate INITIALLY clause"); + requireDdlSyntax("IMMEDIATE".equalsIgnoreCase(token.image) || "DEFERRED".equalsIgnoreCase(token.image), + "Expected IMMEDIATE or DEFERRED"); + attributes.setInitially(ConstraintAttributes.Initially.valueOf(token.image.toUpperCase(Locale.ROOT))); + present = true; } | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.UNKNOWN); + LOOKAHEAD(2) enforced=ConstraintEnforcement() { + requireDdlSyntax(attributes.getEnforced() == null, "Duplicate ENFORCED clause"); + attributes.setEnforced(enforced); present = true; } | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.ERROR); + LOOKAHEAD({ getToken(1).kind == K_NOT && "VALID".equalsIgnoreCase(getToken(2).image) }) + { + requireDdlSyntax(!attributes.isNotValid(), "Duplicate NOT VALID clause"); + attributes.setNotValid(true); present = true; } - ) - { - return behavior; - } + ) )* + { return present ? attributes : null; } } -JsonFunction JsonExistsBody() : { - JsonFunction result = new JsonFunction(JsonFunctionType.EXISTS); - JsonFunctionExpression inputExpression; - Expression expression; - JsonFunction.JsonOnResponseBehavior behavior; -} +Boolean ConstraintEnforcement(): +{ boolean enforced = true; } { - "(" - inputExpression = JsonValueOrQueryInputExpression() { result.setInputExpression(inputExpression); } - "," - expression = Expression() { result.setJsonPathExpression(expression); } + [ { enforced = false; } ] + { return enforced; } +} - [ - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) - JsonKeyword("PASSING") - expression = Expression() { result.addPassingExpression(expression); } - ( LOOKAHEAD(2) "," expression = Expression() { result.addPassingExpression(expression); } )* - ] +/** + * Parses a single table-level constraint inside {@code CREATE TABLE (...)}. Index forms delegate + * to {@link #TableIndexSpec(boolean)} so CREATE and ALTER expose the same structured AST. + */ +TableElement CreateTableConstraint(): +{ TableElement index; } +{ index=TableConstraint(true) { return index; } } - [ - LOOKAHEAD( JsonExistsOnResponseBehavior() ) - behavior = JsonExistsOnResponseBehavior() - - { result.setOnErrorBehavior(behavior); } - ] - ")" - { - return result; - } +/** Attaches an existing PostgreSQL index without declaring new index columns. */ +ConstraintUsingIndex PostgreSqlConstraintUsingIndex(): +{ ConstraintUsingIndex constraint = new ConstraintUsingIndex(); String name; } +{ + [ name=RelObjectName() { constraint.setName(name); } ] + ( { constraint.setType("UNIQUE"); } + | { constraint.setType("PRIMARY KEY"); } ) + name=RelObjectName() { constraint.setExistingIndexName(name); } + PostgreSqlConstraintAttributes(constraint) + { return constraint; } } -JsonFunction JsonValueBody() : { - JsonFunction result = new JsonFunction(JsonFunctionType.VALUE); - JsonFunctionExpression inputExpression; - Expression expression; - ColDataType dataType; - JsonFunction.JsonOnResponseBehavior behavior; +/** Shared table-constraint body; index option boundaries depend on CREATE versus ALTER. */ +TableElement TableConstraint(boolean createContext): +{ + String constraintName = null; + TableElement index = null; + NamedConstraint namedConstraint; + ForeignKeyConstraint fkIndex = null; + CheckConstraint checkConstraint = null; } { - "(" - inputExpression = JsonValueOrQueryInputExpression() { result.setInputExpression(inputExpression); } - "," - expression = Expression() { result.setJsonPathExpression(expression); } + ( + LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(createContext) + | + + [ LOOKAHEAD({ !isTableIndexAhead() && getToken(1).kind != K_FOREIGN + && getToken(1).kind != K_CHECK && getToken(1).kind != K_EXCLUDE + && !(getToken(1).kind == K_NOT && getToken(2).kind == K_NULL) }) constraintName=RelObjectName() ] + ( + LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(createContext) { + if (index instanceof NamedConstraint) { + ((NamedConstraint) index).setUseConstraintKeyword(true); + ((NamedConstraint) index).setName(constraintName); + } + } + | + fkIndex=ForeignKeySpec(constraintName) { + fkIndex.setUseConstraintKeyword(true); + index = fkIndex; + } + | + checkConstraint=CheckConstraintSpec(constraintName) { + checkConstraint.setUseConstraintKeyword(true); + index = checkConstraint; + } + | + LOOKAHEAD({ isExcludeConstraintAhead() }) + namedConstraint=PostgreSqlExcludeConstraint() { namedConstraint.setName(constraintName); index = namedConstraint; } + | + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_NOT }) + namedConstraint=PostgreSqlNotNullConstraint() { namedConstraint.setName(constraintName); index = namedConstraint; } + ) + | + fkIndex=ForeignKeySpec(null) { index = fkIndex; } + | + checkConstraint=CheckConstraintSpec(null) { index = checkConstraint; } + | + LOOKAHEAD({ isExcludeConstraintAhead() }) index=PostgreSqlExcludeConstraint() + | + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_NOT }) + index=PostgreSqlNotNullConstraint() + ) + PostgreSqlConstraintAttributes(index) + { return index; } +} - [ - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) - JsonKeyword("PASSING") - expression = Expression() { result.addPassingExpression(expression); } - ( LOOKAHEAD(2) "," expression = Expression() { result.addPassingExpression(expression); } )* - ] +/** PostgreSQL 18 adds a table-level NOT NULL with a named target column. */ +NotNullConstraint PostgreSqlNotNullConstraint(): +{ NotNullConstraint constraint = new NotNullConstraint(); String name; boolean noInherit; } +{ + name=RelObjectName() { constraint.setColumn(linkToLastToken(createColumn(name))); } + noInherit=PostgreSqlNoInheritClause() { constraint.setNoInherit(noInherit); } + { return constraint; } +} - [ dataType = ColDataType() { result.setReturningType(dataType); } ] +NotNullConstraint PostgreSqlColumnNotNullConstraint(): +{ String name = null; boolean noInherit; } +{ + [ name=RelObjectName() ] + noInherit=PostgreSqlNoInheritClause() + { return new NotNullConstraint().withColumnConstraint(true).withName(name).withNoInherit(noInherit); } +} - [ - LOOKAHEAD( JsonValueOnResponseBehavior() ) - behavior = JsonValueOnResponseBehavior() - - { result.setOnEmptyBehavior(behavior); } - ] +boolean PostgreSqlNoInheritClause(): +{ boolean noInherit = false; } +{ + [ LOOKAHEAD({ getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) }) + TypeDdlKeyword("INHERIT") { noInherit = true; } ] + { return noInherit; } +} - [ - LOOKAHEAD( JsonValueOnResponseBehavior() ) - behavior = JsonValueOnResponseBehavior() - - { result.setOnErrorBehavior(behavior); } - ] - ")" +ExcludeConstraint PostgreSqlExcludeConstraint(): +{ + ExcludeConstraint constraint = new ExcludeConstraint(); + String using; + List columns = new ArrayList(); + KeyElement column; + Expression predicate; +} +{ + + [ LOOKAHEAD(2) using=RelObjectName() { constraint.setUsing(using); } ] + [ LOOKAHEAD(2) "(" column=PostgreSqlExcludeElement() { columns.add(column); } + ( "," column=PostgreSqlExcludeElement() { columns.add(column); } )* ")" + { constraint.setColumns(columns); } ] + PostgreSqlConstraintOptions(constraint) + [ "(" predicate=Expression() ")" { constraint.setExpression(predicate); } ] { - return result; + requireDdlSyntax(constraint.getColumns() != null || constraint.getExpression() != null, + "Expected exclusion elements or predicate"); + return constraint; } } -JsonFunction JsonQueryBody() : { - JsonFunction result = new JsonFunction(JsonFunctionType.QUERY); - JsonFunctionExpression inputExpression; - Expression expression; - ColDataType dataType; - JsonFunction.JsonOnResponseBehavior behavior; - Token token; - String encoding; - ColDataType additionalReturningType; - boolean additionalReturningFormatJson; - String additionalReturningEncoding; - JsonFunction.JsonWrapperType additionalWrapperType; - JsonFunction.JsonWrapperMode additionalWrapperMode; - boolean additionalWrapperArray; - JsonFunction.JsonQuotesType additionalQuotesType; - boolean additionalQuotesOnScalarString; - JsonFunction.JsonOnResponseBehavior additionalOnEmptyBehavior; - JsonFunction.JsonOnResponseBehavior additionalOnErrorBehavior; - StringBuilder additionalBuilder; - boolean hasPassingClause = false; +KeyElement PostgreSqlExcludeElement(): +{ + KeyElement column; + Token operator; + ExclusionOperator reference; } { - "(" - inputExpression = JsonValueOrQueryInputExpression() { result.setInputExpression(inputExpression); } - "," - expression = Expression() { result.setJsonPathExpression(expression); } + column=IndexColumnWithParams() + ( + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && isKeywordAhead("OPERATOR") }) + TypeDdlKeyword("OPERATOR") "(" reference=PostgreSqlOperatorReference() ")" { + reference.setUseOperatorKeyword(true); + column.setExclusionOperatorReference(reference); + } + | + ( operator="=" | operator= | operator= | operator="<" | operator=">" + | operator= | operator= | operator="&&" | operator="@>" | operator="<@" ) + { column.setExclusionOperator(operator.image); } + ) + { return column; } +} + + +CreateTable CreateTable(boolean isUsingOrReplace): +{ + CreateTable createTable = new CreateTable(); + Token ofFirst; + Token executeFirst; + Table table = null; + List tableElements = new ArrayList(); + TableElement element; + List typedTableOptions = new ArrayList(); + List createOptions = new ArrayList(); + Token tk = null; + List parameter = new ArrayList(); + TableOption tableOption = null; + SpannerInterleaveIn interleaveIn = null; + Select select = null; + Execute execute; + int optionCount; + Table likeTable = null; + RowMovement rowMovement = null; + String tableColumn = null; + List columns = new ArrayList(); + TablePartitioning partitioning = null; + Table partitionOfTable = null; + PartitionBound partitionBound = null; + ColDataType ofType = null; + Table parent; + List
parents = new ArrayList
(); + ObjectNames typeName; + boolean withData; + boolean foreign = false; + ForeignTableOptions foreignOptions; +} +{ + { createTable.setOrReplace(isUsingOrReplace);} + [ { createTable.setUnlogged(true); } ] + + // table options, not required but 1 or none + [ ( tk= | tk= ) { createOptions.add(tk.image);} ] + (parameter = CreateParameter() { createOptions.addAll(parameter); })* + + { for (String opt : createOptions) { if ("FOREIGN".equalsIgnoreCase(opt)) { foreign = true; } } } + + [ LOOKAHEAD(2) { createTable.setIfNotExists(true); }] + table=Table() [ - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) - JsonKeyword("PASSING") { hasPassingClause = true; } - expression = Expression() { result.addPassingExpression(expression); } - ( LOOKAHEAD(2) "," expression = Expression() { result.addPassingExpression(expression); } )* + // BigQuery: CREATE [SNAPSHOT] TABLE target CLONE source [FOR SYSTEM_TIME AS OF ...] + LOOKAHEAD(2) likeTable=Table() + [ LOOKAHEAD(2) tableColumn=TimeTravelAfterAlias() { likeTable.setTimeTravel(tableColumn); } ] + { createTable.setCloneTable(likeTable); likeTable = null; } ] + [ { ofFirst = getToken(1); } typeName=RelObjectNames() { + ofType = new ColDataType(); + ofType.setDataType(typeName.getNames()); + linkAST(ofType, ofFirst); + createTable.setOfType(ofType); + } ] + [ LOOKAHEAD(2) partitionOfTable=Table() + { createTable.setPartitionOf(partitionOfTable); } ] + [ LOOKAHEAD(2) ( + LOOKAHEAD("(" ")") "(" ")" { createTable.setTableElements(tableElements); } + | + LOOKAHEAD("(" RelObjectName() ("," RelObjectName())* ")") ( + "(" tableColumn=RelObjectName() { columns.add(tableColumn); } ("," tableColumn=RelObjectName() { columns.add(tableColumn); } )* ")" + ) + | + ( + "(" element=CreateTableElement(ofType != null || partitionOfTable != null) { tableElements.add(element); } + ( LOOKAHEAD(2) "," element=CreateTableElement(ofType != null || partitionOfTable != null) + { tableElements.add(element); } + )* + [ LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_COMMA && getToken(2).kind == CLOSING_BRACKET }) "," ] - [ - dataType = ColDataType() { result.setReturningType(dataType); } - [ - { result.setReturningFormatJson(true); } - [ encoding = JsonEncoding() { result.setReturningEncoding(encoding); } ] - ] + ")" + ) + ) ] + [ LOOKAHEAD({ isKeywordAhead("INHERITS") }) ContextualKeyword("INHERITS") + "(" parent=Table() { parents.add(parent); } + ( "," parent=Table() { parents.add(parent); } )* ")" + { createTable.setInherits(parents); } ] + [ LOOKAHEAD({ partitionOfTable != null }) + partitionBound=PostgreSqlPartitionBound() + { createTable.setPartitionBound(partitionBound); } ] + [ LOOKAHEAD({ isKeywordAhead("SERVER") }) foreignOptions=ForeignTableOptions() + { requireDdlSyntax(foreign, "SERVER requires CREATE FOREIGN TABLE"); + typedTableOptions.add(TableOption.foreignServer(foreignOptions)); } ] + CreateTableOptions(typedTableOptions) + [ LOOKAHEAD( ) partitioning=CreateTablePartitioning() + { createTable.setPartitioning(partitioning); } ] + { optionCount = typedTableOptions.size(); } + CreateTableOptions(typedTableOptions) + { createTable.setTableOptionsAfterPartition(typedTableOptions.size() > optionCount); } - [ + // see https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_7002.htm#i2126725 + // table properties , all these are optional + [ rowMovement = RowMovement() { createTable.setRowMovement(rowMovement); }] + [ LOOKAHEAD(2) + { createTable.setUseAsKeyword(false); } + [ ( tk= | tk= ) + { createTable.setDuplicateHandling(CreateTable.DuplicateHandling.valueOf( + tk.image.toUpperCase(Locale.ROOT))); } ] + [ { createTable.setUseAsKeyword(true); } ] ( - { result.setWrapperType(JsonFunction.JsonWrapperType.WITHOUT); } - [ { result.setWrapperArray(true); } ] - JsonKeyword("WRAPPER") + LOOKAHEAD(1, ) { executeFirst = getToken(1); } execute=Execute() { + linkStatement(execute, executeFirst); + requireDdlSyntax(createTable.isUseAsKeyword() && createTable.getDuplicateHandling() == null, + "CREATE TABLE EXECUTE requires AS and cannot use duplicate handling"); + createTable.setExecute(execute); + } | - { result.setWrapperType(JsonFunction.JsonWrapperType.WITH); } - [ - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && (getToken(1).image.equalsIgnoreCase("CONDITIONAL") - || getToken(1).image.equalsIgnoreCase("UNCONDITIONAL")) - }) - token = - { - if (token.image.equalsIgnoreCase("CONDITIONAL")) { - result.setWrapperMode(JsonFunction.JsonWrapperMode.CONDITIONAL); - } else { - result.setWrapperMode(JsonFunction.JsonWrapperMode.UNCONDITIONAL); - } - } - ] - [ { result.setWrapperArray(true); } ] - JsonKeyword("WRAPPER") + select=Select() + { createTable.setSelect(select, false); } ) + [ LOOKAHEAD(2) withData=DataPopulation() + { createTable.setWithData(withData); } ] + ] + [ + ( LOOKAHEAD("(" Table() ")") "(" likeTable=Table() { createTable.setLikeTable(likeTable, true); } ")" + | likeTable=Table() { createTable.setLikeTable(likeTable, false); } ) + ] + [ interleaveIn = SpannerInterleaveIn( ) { createTable.setSpannerInterleaveIn(interleaveIn); } ] + { + createTable.setTable(table); + if (tableElements.size() > 0) + createTable.setTableElements(tableElements); + if (createOptions.size() > 0) + createTable.setCreateOptionsStrings(createOptions); + if (typedTableOptions.size() > 0) + createTable.setTableOptions(typedTableOptions); + if (!columns.isEmpty()) { + if (ofType != null || partitionOfTable != null) { + for (String name : columns) { + tableElements.add(new ColumnDefinition().withColumnName(name)); + } + createTable.setTableElements(tableElements); + } else { + createTable.setColumns(columns); + } + } + requireDdlSyntax(!foreign + || createTable.getForeignTableOptions() != null, "CREATE FOREIGN TABLE requires SERVER"); + return createTable; + } +} + +ForeignTableOptions ForeignTableOptions(): +{ + ForeignTableOptions result = new ForeignTableOptions(); + String name; + List options; +} +{ + ContextualKeyword("SERVER") name=RelObjectName() { result.setServer(name); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(false) { result.setOptions(options); } ] + { return result; } +} + +List ForeignDataOptions(boolean alter): +{ + List options = new ArrayList(); + ForeignDataOption option; +} +{ + ContextualKeyword("OPTIONS") "(" option=ForeignDataOption() { options.add(option); } + ( "," option=ForeignDataOption() { options.add(option); } )* ")" + { + for (ForeignDataOption item : options) { + requireDdlSyntax(alter || item.getAction() == null, "CREATE options do not allow ADD, SET or DROP"); + } + return options; + } +} + +ForeignDataOption ForeignDataOption(): +{ + ForeignDataOption.Action action = null; + String name; + Token value; + ForeignDataOption option; +} +{ + ( + LOOKAHEAD(1, ) name=RelObjectName() + { option = new ForeignDataOption(name, null); option.setAction(ForeignDataOption.Action.DROP); } + | + [ LOOKAHEAD(1) ( { action = ForeignDataOption.Action.ADD; } + | { action = ForeignDataOption.Action.SET; } ) ] + name=RelObjectName() value= + { option = new ForeignDataOption(name, stringValue(value)); option.setAction(action); } + ) + { return option; } +} + +TableElement CreateTableElement(boolean typed): +{ TableElement element; } +{ + ( + LOOKAHEAD(1, ) element=LikeClause() + | LOOKAHEAD(3) element=CreateTableConstraint() + | element=CreateTableColumnDefinition(typed) + ) + { return element; } +} + +ColumnDefinition CreateTableColumnDefinition(boolean typed): +{ + ColumnDefinition column = null; + String name; + List options = new ArrayList(); + ColumnOption option; + Token token; +} +{ + { if (!typed) { return ColumnDefinition(); } } + name=RelObjectName() + { column = new ColumnDefinition().withColumnName(name); } + [ LOOKAHEAD(2) token= { + if (!"OPTIONS".equalsIgnoreCase(token.image)) { + throw new ParseException("Expected OPTIONS after WITH"); + } + column.setWithOptions(true); + } ] + ( LOOKAHEAD(2) option=ColumnDefinitionOption() { options.add(option); } )* + { column.setColumnOptions(options); } + { if (true) { return column; } } +} + +/** Reads complete options and keeps commas separate from legacy raw token groups. */ +void CreateTableOptions(List options): +{ TableOption option; } +{ + [ LOOKAHEAD(2, { !isCreateTableQueryPrefix() + && !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) }) + option=CreateTableOption() { options.add(option); } + ( LOOKAHEAD(2, { !isCreateTableQueryPrefix() + && !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) + && !(getToken(1).kind == K_COMMA + && "INTERLEAVE".equalsIgnoreCase(getToken(2).image)) }) + [ "," ] option=CreateTableOption() { options.add(option); } + )* ] +} - [ - LOOKAHEAD({ - getToken(1).kind == K_KEEP - || (getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("OMIT")) - }) +TableOption CreateTableOption(): +{ TableOption option; List tokens; List storageParameters; String name; } +{ + ( + LOOKAHEAD( "(") storageParameters=PostgreSqlIndexOptions() { + option = new TableOption(); option.setStorageParameters(storageParameters); + } + | + LOOKAHEAD( ) ContextualKeyword("OIDS") { + option = new TableOption(TableOption.Kind.WITHOUT_OIDS, "WITHOUT OIDS", null, false); + } + | + LOOKAHEAD( ) option=OnCommitTableOption() + | + // USING INDEX TABLESPACE stays a raw parameter + LOOKAHEAD(2, { getToken(2).kind != K_INDEX }) name=RelObjectName() + { option = TableOption.accessMethod(name); } + | + LOOKAHEAD({ isMySqlTableOptionAhead() }) option=MySqlTableOption() + | + // Snowflake: COPY is reserved because it starts the COPY statement + LOOKAHEAD({ getToken(1).kind == K_COPY && "GRANTS".equalsIgnoreCase(getToken(2).image) }) + ContextualKeyword("GRANTS") { option = TableOption.raw("COPY", "GRANTS"); } + | tokens=CreateParameter() { option = TableOption.raw(tokens); } ) + { return option; } +} + +/** Temporary table ON COMMIT action, including Oracle's private temporary table forms. */ +TableOption OnCommitTableOption(): +{ TableOption.OnCommitAction action; } +{ + + ( + { action = TableOption.OnCommitAction.DELETE_ROWS; } + | + { action = TableOption.OnCommitAction.DROP; } + [ LOOKAHEAD({ isKeywordAhead("DEFINITION") }) ContextualKeyword("DEFINITION") + { action = TableOption.OnCommitAction.DROP_DEFINITION; } ] + | + LOOKAHEAD({ isKeywordAhead("PRESERVE") }) ContextualKeyword("PRESERVE") ( - { result.setQuotesType(JsonFunction.JsonQuotesType.KEEP); } + { action = TableOption.OnCommitAction.PRESERVE_ROWS; } | - JsonKeyword("OMIT") { result.setQuotesType(JsonFunction.JsonQuotesType.OMIT); } + ContextualKeyword("DEFINITION") { action = TableOption.OnCommitAction.PRESERVE_DEFINITION; } ) - JsonKeyword("QUOTES") - [ - JsonKeyword("SCALAR") - { result.setQuotesOnScalarString(true); } - ] - ] - - [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) - behavior = JsonQueryOnResponseBehavior() - - { result.setOnEmptyBehavior(behavior); } - ] - - [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) - behavior = JsonQueryOnResponseBehavior() - - { result.setOnErrorBehavior(behavior); } - ] + ) + { return TableOption.onCommit(action); } +} +TableOption MySqlTableOption(): { + Token tk; + Token tk2 = null; + String value = null; + List
unionTables = null; + Table unionTable; + boolean useEquals = false; + String name = ""; + TableOption.Kind kind = null; + TableOption option; + ColumnOption.Storage storage = null; + StringValue comment = null; +} { ( - LOOKAHEAD(2, { !hasPassingClause }) - "," - { - additionalReturningType = null; - additionalReturningFormatJson = false; - additionalReturningEncoding = null; - additionalWrapperType = null; - additionalWrapperMode = null; - additionalWrapperArray = false; - additionalQuotesType = null; - additionalQuotesOnScalarString = false; - additionalOnEmptyBehavior = null; - additionalOnErrorBehavior = null; + tk= { name = tk.image; kind = TableOption.Kind.UNION; + unionTables = new ArrayList
(); } + [ "=" { useEquals = true; } ] + "(" [ unionTable=Table() { unionTables.add(unionTable); } + ( "," unionTable=Table() { unionTables.add(unionTable); } )* ] ")" + | + LOOKAHEAD({ isMySqlStatisticsOptionAhead() }) tk= { + name = tk.image; + kind = TableOption.Kind.valueOf(name.toUpperCase(Locale.ROOT)); } - expression = Expression() - [ - additionalReturningType = ColDataType() - [ - { additionalReturningFormatJson = true; } - [ additionalReturningEncoding = JsonEncoding() ] - ] - ] - [ - ( - - { additionalWrapperType = JsonFunction.JsonWrapperType.WITHOUT; } - [ { additionalWrapperArray = true; } ] - JsonKeyword("WRAPPER") - | - - { additionalWrapperType = JsonFunction.JsonWrapperType.WITH; } - [ - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && (getToken(1).image.equalsIgnoreCase("CONDITIONAL") - || getToken(1).image.equalsIgnoreCase("UNCONDITIONAL")) - }) - token = - { - if (token.image.equalsIgnoreCase("CONDITIONAL")) { - additionalWrapperMode = JsonFunction.JsonWrapperMode.CONDITIONAL; - } else { - additionalWrapperMode = JsonFunction.JsonWrapperMode.UNCONDITIONAL; - } - } - ] - [ { additionalWrapperArray = true; } ] - JsonKeyword("WRAPPER") - ) - ] - [ - LOOKAHEAD({ - getToken(1).kind == K_KEEP - || (getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("OMIT")) - }) - ( - { additionalQuotesType = JsonFunction.JsonQuotesType.KEEP; } - | - JsonKeyword("OMIT") { additionalQuotesType = JsonFunction.JsonQuotesType.OMIT; } - ) - JsonKeyword("QUOTES") - [ - JsonKeyword("SCALAR") { additionalQuotesOnScalarString = true; } - ] - ] - [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) - additionalOnEmptyBehavior = JsonQueryOnResponseBehavior() - - ] - [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) - additionalOnErrorBehavior = JsonQueryOnResponseBehavior() - - ] - { - additionalBuilder = new StringBuilder(); - additionalBuilder.append(expression); - if (additionalReturningType != null) { - additionalBuilder.append(" RETURNING ").append(additionalReturningType); - if (additionalReturningFormatJson) { - additionalBuilder.append(" FORMAT JSON"); - if (additionalReturningEncoding != null) { - additionalBuilder.append(" ENCODING ").append(additionalReturningEncoding); - } - } - } - if (additionalWrapperType != null) { - additionalBuilder.append(" ").append(additionalWrapperType); - if (additionalWrapperMode != null) { - additionalBuilder.append(" ").append(additionalWrapperMode); - } - if (additionalWrapperArray) { - additionalBuilder.append(" ARRAY"); - } - additionalBuilder.append(" WRAPPER"); - } - if (additionalQuotesType != null) { - additionalBuilder.append(" ").append(additionalQuotesType).append(" QUOTES"); - if (additionalQuotesOnScalarString) { - additionalBuilder.append(" ON SCALAR STRING"); - } + [ "=" { useEquals = true; } ] + ( tk= | tk= ) { value = tk.image; + requireDdlSyntax(kind == TableOption.Kind.STATS_SAMPLE_PAGES + || "DEFAULT".equalsIgnoreCase(value) || "0".equals(value) || "1".equals(value), + "Expected DEFAULT, 0, or 1 for " + name); + } + | + LOOKAHEAD({ isKeywordAhead("ROW_FORMAT") }) ContextualKeyword("ROW_FORMAT") + { name = "ROW_FORMAT"; kind = TableOption.Kind.ROW_FORMAT; } + [ "=" { useEquals = true; } ] value=RelObjectName() { + requireDdlSyntax(Arrays.asList("DEFAULT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT", "COMPACT") + .contains(value.toUpperCase(Locale.ROOT)), "Invalid MySQL ROW_FORMAT"); + } + | + LOOKAHEAD({ isMySqlScalarTableOptionAhead() }) option=MySqlScalarTableOption() + { return option; } + | + { name = "TABLESPACE"; kind = TableOption.Kind.TABLESPACE; } + [ "=" { useEquals = true; } ] value=RelObjectName() + [ LOOKAHEAD(2) tk= { + requireDdlSyntax("DISK".equalsIgnoreCase(tk.image) || "MEMORY".equalsIgnoreCase(tk.image), + "Expected DISK or MEMORY after STORAGE"); + storage = ColumnOption.Storage.valueOf(tk.image.toUpperCase(Locale.ROOT)); + } ] + | + tk= { + name = tk.image; + kind = TableOption.Kind.ENGINE; + } + [ "=" { useEquals = true; } ] + value=MySqlTableOptionValue() + | + [ tk= { name = tk.image + " "; } ] + ( + tk= tk2= { + name += tk.image + " " + tk2.image; } - if (additionalOnEmptyBehavior != null) { - additionalBuilder.append(" ").append(additionalOnEmptyBehavior).append(" ON EMPTY"); + | + tk= tk2= { + name += tk.image + " " + tk2.image; } - if (additionalOnErrorBehavior != null) { - additionalBuilder.append(" ").append(additionalOnErrorBehavior).append(" ON ERROR"); + | + tk= { name += tk.image; } + | + tk= { + name += tk.image; + kind = TableOption.Kind.COLLATE; } - result.addAdditionalQueryPathArgument(additionalBuilder.toString()); + ) + { if (kind == null) { kind = TableOption.Kind.CHARACTER_SET; } } + [ "=" { useEquals = true; } ] + value=MySqlTableOptionValue() + | + tk= { + name = tk.image; + kind = TableOption.Kind.COMMENT; } - )* - ")" + [ "=" { useEquals = true; } ] + // A string literal keeps its escaping; other values remain plain text + ( + LOOKAHEAD({ getToken(1).kind == S_CHAR_LITERAL }) tk= + { comment = stringValue(tk); } + | value=MySqlTableOptionValue() + ) + ) { - return result; + option = new TableOption(kind, name, value, useEquals); + if (comment != null) { option.setComment(comment); } + if (unionTables != null) { option.setUnionTables(unionTables); } + option.setTablespaceStorage(storage); + return option; } } -JsonFunction JsonFunction() : { - JsonFunction result; +/** Shared CREATE/ALTER table options with option-specific value syntax. */ +TableOption MySqlScalarTableOption(): +{ + Token token; + String name; + String value; + TableOption.Kind kind; + boolean useEquals = false; + TableOption additional; } { ( - ( result = JsonObjectBody() ) - | - ( result = JsonArrayBody() ) - | - ( - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("JSON_VALUE") - }) - JsonKeyword("JSON_VALUE") - result = JsonValueBody() - ) - | - ( - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("JSON_QUERY") - }) - JsonKeyword("JSON_QUERY") - result = JsonQueryBody() - ) - | - ( - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("JSON_EXISTS") - }) - JsonKeyword("JSON_EXISTS") - result = JsonExistsBody() - ) + LOOKAHEAD({ isMySqlNumericTableOptionAhead() }) + ( token= | token= | token= | token= ) { + name = token.image.toUpperCase(Locale.ROOT); kind = TableOption.Kind.valueOf(name); + } + [ "=" { useEquals = true; } ] token= { value = token.image; } + | + LOOKAHEAD({ isMySqlAdditionalTableOptionAhead() }) additional=MySqlAdditionalTableOption() + { name = additional.getName(); kind = additional.getKind(); + value = additional.getValue(); useEquals = additional.isUseEquals(); } + | + { name = "AUTO_INCREMENT"; kind = TableOption.Kind.AUTO_INCREMENT; } + [ "=" { useEquals = true; } ] token= { value = token.image; } + | + ( + { name = "ENCRYPTION"; kind = TableOption.Kind.ENCRYPTION; } + | { name = "PASSWORD"; kind = TableOption.Kind.PASSWORD; } + | { name = "DATA DIRECTORY"; kind = TableOption.Kind.DATA_DIRECTORY; } + | { name = "INDEX DIRECTORY"; kind = TableOption.Kind.INDEX_DIRECTORY; } + | LOOKAHEAD({ isMySqlEngineAttributeAhead() }) token= { + name = token.image.toUpperCase(Locale.ROOT); + kind = TableOption.Kind.valueOf(name); + } + ) + [ "=" { useEquals = true; } ] token= { value = token.image; } + ) + { return new TableOption(kind, name, value, useEquals); } +} + +TableOption MySqlAdditionalTableOption(): +{ + Token token; + String name; + String value; + boolean useEquals = false; +} +{ + ( + LOOKAHEAD({ isKeywordAhead("SECONDARY_ENGINE") }) + token= { name = token.image.toUpperCase(Locale.ROOT); } + [ "=" { useEquals = true; } ] + ( token= { value = token.image; } | token= { value = token.image; } | value=RelObjectName() ) + | + LOOKAHEAD({ isKeywordAhead("INSERT_METHOD") }) + token= { name = token.image.toUpperCase(Locale.ROOT); } + [ "=" { useEquals = true; } ] + ( token= | token= | token= ) { value = token.image; } + | + LOOKAHEAD({ isKeywordAhead("PACK_KEYS") }) + token= { name = token.image.toUpperCase(Locale.ROOT); } + [ "=" { useEquals = true; } ] + ( token= | token= ) { + value = token.image; + if (token.kind == S_LONG && !"0".equals(value) && !"1".equals(value)) { + throw new ParseException("PACK_KEYS requires 0, 1 or DEFAULT"); + } + } + | + LOOKAHEAD({ isKeywordAhead("AUTOEXTEND_SIZE") }) + token= { name = token.image.toUpperCase(Locale.ROOT); } + [ "=" { useEquals = true; } ] + value=MySqlSizeNumber() + | + LOOKAHEAD({ isKeywordAhead("CHECKSUM") || isKeywordAhead("DELAY_KEY_WRITE") }) + ( token= | token= ) { name = token.image.toUpperCase(Locale.ROOT); } + [ "=" { useEquals = true; } ] + token= { value = token.image; } + | + token= { name = token.image.toUpperCase(Locale.ROOT); } + [ "=" { useEquals = true; } ] + token= { value = token.image; } ) + { if (true) { return new TableOption(TableOption.Kind.valueOf(name), name, value, useEquals); } } +} + +String MySqlSizeNumber(): +{ + Token number; +} +{ + ( number= | number= ) { - return result; + requireDdlSyntax(number.kind == S_LONG || number.image.matches("[0-9]+[kKmMgG]"), + "Expected an unsigned size with an optional K, M or G suffix"); + return number.image; } } -JsonAggregateFunction JsonAggregateFunction() : { - JsonAggregateFunction result = new JsonAggregateFunction(); - Token token; - Object key; - Expression expression; - List expressionOrderByList = null; +/** Adapts CREATE's option nodes to the existing ALTER option APIs. */ +AlterExpression AlterSharedTableOption(): +{ + TableOption option; + TableOption collation = null; + AlterExpressionCharset charset; + AlterExpressionTableOption action; +} +{ + option=MySqlTableOption() + [ LOOKAHEAD({ option.getKind() == TableOption.Kind.CHARACTER_SET && getToken(1).kind == K_COLLATE }) + collation=MySqlTableOption() ] + { + if (option.getKind() == TableOption.Kind.CHARACTER_SET || option.getKind() == TableOption.Kind.COLLATE) { + charset = new AlterExpressionCharset(); + boolean useDefault = option.getName().toUpperCase(Locale.ROOT).startsWith("DEFAULT "); + if (option.getKind() == TableOption.Kind.CHARACTER_SET) { + charset.setOperation(AlterOperation.CONVERT); + charset.setConvertType(useDefault ? AlterExpression.ConvertType.DEFAULT_CHARACTER_SET + : AlterExpression.ConvertType.CHARACTER_SET); + charset.setCharacterSet(option.getValue()); + charset.setUseCharsetKeyword(option.getName().toUpperCase(Locale.ROOT).endsWith("CHARSET")); + charset.setHasEqualForCharacterSet(option.isUseEquals()); + if (collation != null) { + charset.setCollation(collation.getValue()); + charset.setHasEqualForCollate(collation.isUseEquals()); + } + } else { + charset.setOperation(AlterOperation.COLLATE); + charset.setCollation(option.getValue()); + charset.setHasEqualForCollate(option.isUseEquals()); + charset.setDefaultCollateSpecified(useDefault); + } + return charset; + } + action = new AlterExpressionTableOption(); + action.setStructuredTableOption(option); + return action; + } +} - Expression filter; - ExpressionList expressionList = null; - List olist = null; - WindowElement windowElement = null; - boolean partitionByBrackets = false; +AlterExpressionOrderBy MySqlAlterOrderBy(): +{ AlterExpressionOrderBy action = new AlterExpressionOrderBy(); OrderByElement element; } +{ + element=MySqlAlterOrderColumn() { action.getOrderByElements().add(element); } + ( LOOKAHEAD(1) "," element=MySqlAlterOrderColumn() { action.getOrderByElements().add(element); } )* + { return action; } } + +OrderByElement MySqlAlterOrderColumn(): +{ OrderByElement element = new OrderByElement(); String name; Token order; Token first = getToken(1); } { + name=RelObjectName() { element.setExpression(linkToLastToken(createColumn(name))); } + [ LOOKAHEAD(2) ( order= | order= ) { element.setAsc(order.kind == K_ASC); element.setAscDescPresent(true); } ] + { linkAST(element, first); return element; } +} + +String MySqlTableOptionValue(): { + Token token; + String value = null; +} { ( - ( - ( - "(" { result.setType( JsonFunctionType.OBJECT ); } + value=RelObjectName() + | token= { value = token.image; } + | token= { value = token.image; } + | token= { value = token.image; } + | token= { value = token.image; } + | token= { value = token.image; } + ) + { return value; } +} + +SpannerInterleaveIn SpannerInterleaveIn(): +{ + Table table = null; + SpannerInterleaveIn.OnDelete action = null; +} +{ + table = Table() + [ + ( - LOOKAHEAD(2) ( - "KEY" { result.setUsingKeyKeyword( true ); } - ( - ( token = | token = | token = | token = | token = ) - { - key = token.image; - } - | - key = Column() - ) - ) - | - ( token = | token = | token = | token = | token = ) - { - key = token.image; - } - | - key = Column() + { action = SpannerInterleaveIn.OnDelete.NO_ACTION; } + | { action = SpannerInterleaveIn.OnDelete.CASCADE; } ) - { - result.setKey( key ); - } + ] + { + return new SpannerInterleaveIn(table, action); + } +} - ( ":" | "," { result.setType( JsonFunctionType.MYSQL_OBJECT ); } | "VALUE" {result.setUsingValueKeyword( true ); } ) - expression = Expression() { result.setValue( expression ); } - [ { result.setUsingFormatJson( true ); } ] +ColDataType DataType(): +{ + ColDataType colDataType = new ColDataType(); + Token prefix = null; + Token tk = null; + Token tk2 = null; + String schema; + String type=""; + List argumentsStringList = new ArrayList(); + List array = new ArrayList(); + List name; + ColDataType arrayType; + Integer zonePrecision = null; + String precision = null; + Integer scale = null; + String scaleText; + Token first = getToken(1); +} +{ + ( + LOOKAHEAD(2) tk= { + type = tk.image; + return linked(ColDataType.fromTypeParameters(type, precision, scale), first); + } + | + LOOKAHEAD(2) tk= ( + ("<" arrayType = ColDataType() ">") { + colDataType.setDataType("ARRAY<" + arrayType.getDataType() + ">"); + } + ) + | + ( + ( tk= | tk= | tk = | tk = | tk = + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= ) + { + type = tk.image; + // A DT_ZONE image already contains its parameter, e.g. "TIMESTAMP(3) WITH TIME ZONE". + zonePrecision = tk.kind == DT_ZONE ? zonedTypePrecision(tk.image) : null; + } + ( + // Signedness is parsed by ColDataType after optional precision/scale. Do not + // consume CHARACTER when it starts the trailing CHARACTER SET clause. + LOOKAHEAD(2, { getToken(1).kind != K_CHARACTER || getToken(2).kind != K_SET }) + ( tk = + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= ) { type += " " + tk.image; } + )* [ - LOOKAHEAD(2) ( - { result.setOnNullType( JsonAggregateOnNullType.NULL ); } - ) - | - ( - { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } - ) + LOOKAHEAD(2) "(" ( tk= { precision = tk.image; } | tk= { precision = "MAX"; } + // Oracle's NUMBER(*, s) and NUMBER(*) + | "*" { precision = "*"; requireDdlSyntax("NUMBER".equalsIgnoreCase(type), + "Wildcard precision is only supported for NUMBER"); } ) + [ | ] + [ "," scaleText=SignedIntegerTypeArgument() { scale = Integer.valueOf(scaleText); } ] + ")" ] + { + colDataType = ColDataType.fromTypeParameters(type, precision, scale); + if (zonePrecision != null) { + colDataType.setPrecision(zonePrecision); + } + } + ) + ) - [ - ( - { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); } - ) - | - ( - { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); } - ) - ] - ")" - ) - | - ( - - "(" { result.setType( JsonFunctionType.ARRAY ); } - expression=Expression() { result.setExpression( expression ); } - [ { result.setUsingFormatJson( true ); } ] - [ expressionOrderByList = OrderByElements() { result.setExpressionOrderByElements( expressionOrderByList ); } ] + { + return linked(colDataType, first); + } +} - [ - LOOKAHEAD(2) ( - { result.setOnNullType( JsonAggregateOnNullType.NULL ); } - ) - | - ( - { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } - ) - ] +/** Shared SQL large-object names; length and array suffixes stay in ColDataType. */ +String LargeObjectTypeName(): +{ + Token national = null; + Token type; + Token large; + Token object; +} +{ + [ LOOKAHEAD(1, ) national= ] + ( type= | type= | type= | type= ) + large= object= + { return (national == null ? "" : national.image + " ") + + type.image + " " + large.image + " " + object.image; } +} - ")" - ) - ) +/** Type modifiers accept negative integer constants, but not unary-plus expressions. */ +String SignedIntegerTypeArgument(): +{ + Token sign = null; + Token number; +} +{ + [ sign="-" ] number= + { return (sign == null ? "" : sign.image) + number.image; } +} - // -- Filter - [ LOOKAHEAD(2) "(" { result.setAnalyticType(AnalyticType.FILTER_ONLY); } filter = Expression() { result.setFilterExpression( filter ); } ")" ] +ColDataType ColDataType(): +{ + ColDataType colDataType = new ColDataType(); + Token prefix = null; + Token tk = null; + Token tk2 = null; + String schema; + String type=""; + List argumentsStringList = new ArrayList(); + List array = new ArrayList(); + List name; + ColDataType arrayType; + ColDataType nestedType = null; + String numericArgument = null; + IntervalQualifier intervalQualifier = null; + Integer zonePrecision = null; - // -- OVER - [ LOOKAHEAD(2) - {result.setAnalyticType(AnalyticType.OVER);} + int precision = -1; + int scale = -1; + Token national = null; + Token nationalType = null; + Token varying = null; + ColDataType.TypeModifier typeModifier = null; + XmlTypeModifier xmlTypeModifier = null; + Token first = getToken(1); +} +{ + ( + LOOKAHEAD({ isLargeObjectTypeAhead() }) type=LargeObjectTypeName() + { colDataType.setDataType(type); } + | + LOOKAHEAD({ isXmlTypeName(getToken(1).image) && "(".equals(getToken(2).image) }) + ( tk= | tk= ) + { colDataType.setDataType(tk.image); } + xmlTypeModifier = XmlTypeModifier() { colDataType.setXmlTypeModifier(xmlTypeModifier); } + | + ( + "(" - [ - (LOOKAHEAD(3) expressionList=ComplexExpressionList() - | "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" ) - ] - [olist=OrderByElements() ] - [windowElement = WindowElement() ] + type = RelObjectNameExt() + colDataType = ColDataType() { argumentsStringList.add( type + " " + colDataType.toString()); } + ( + "," + type = RelObjectNameExt() + colDataType = ColDataType() { argumentsStringList.add( type + " " + colDataType.toString()); } + )* + ")" { colDataType = new ColDataType("STRUCT"); } + ) + | + LOOKAHEAD({ isKeywordAhead("NATIONAL") }) ( + national= + (nationalType= | nationalType= | nationalType=) + [ LOOKAHEAD({ isKeywordAhead("VARYING") }) varying= ] { - result.setPartitionExpressionList(expressionList, partitionByBrackets); - result.setOrderByElements(olist); - result.setWindowElement(windowElement); + type = national.image + " " + nationalType.image + + (varying != null ? " " + varying.image : ""); + colDataType.setDataType(type); + colDataType.setNationalCharacterType( + nationalType.image.equalsIgnoreCase("VARCHAR") || varying != null + ? ColDataType.NationalCharacterType.VARCHAR + : ColDataType.NationalCharacterType.CHAR); + } + ) + | + LOOKAHEAD(2) ( + colDataType = DataType() + { + if (isMySqlDialect() + && colDataType.getDataType().toUpperCase(Locale.ROOT).startsWith("NCHAR")) { + colDataType.setNationalCharacterType( + colDataType.getDataType().toUpperCase(Locale.ROOT).contains("VARCHAR") + ? ColDataType.NationalCharacterType.VARCHAR + : ColDataType.NationalCharacterType.CHAR); + } else if (isMySqlDialect() && colDataType.getDataType().toUpperCase(Locale.ROOT) + .startsWith("NVARCHAR")) { + colDataType.setNationalCharacterType(ColDataType.NationalCharacterType.VARCHAR); + } + } + ) + | + ( + tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + | tk= + // PostgreSQL's geometric type + | tk= + // Oracle's RAW(n) + | tk= + ) + { + schema = tk.image; + // A DT_ZONE image already contains its parameter, e.g. "TIMESTAMP(3) WITH TIME ZONE". + zonePrecision = tk.kind == DT_ZONE ? zonedTypePrecision(tk.image) : null; + } + + // Consume an optional INTERVAL qualifier such as `hour to minute` or + // `day(9) to second`. Only applicable when the matched type is an INTERVAL and + // is immediately followed by a field word, so other types (and the existing + // `interval (2)` precision form handled below) are unaffected. + [ + LOOKAHEAD({ "interval".equalsIgnoreCase(schema) + && getToken(1).kind == K_DATE_LITERAL }) + intervalQualifier = IntervalQualifier() { colDataType.setIntervalQualifier(intervalQualifier); } + ] + [ LOOKAHEAD(2) "." arrayType = ColDataType() { + schema += "." + arrayType.getDataType(); + // Retain the nested type's structured suffixes instead of flattening them into its name. + colDataType = arrayType; + } ] + { colDataType.setDataType(schema); } + ) + + [ + LOOKAHEAD(2) "(" + ( + { tk2 = null; numericArgument = null; } + ( + LOOKAHEAD(( | ) "(") nestedType=ColDataType() + | + ( + ( + ( numericArgument=SignedIntegerTypeArgument() | tk= ) [ LOOKAHEAD(2) (tk2= | tk2=) ] + ) + | + tk= + | + tk= + | + // DuckDB parameterises types with other types: MAP(VARCHAR, INTEGER) + tk= + | + tk= + | + tk="*" { requireDdlSyntax("VECTOR".equalsIgnoreCase(colDataType.getDataType()), + "Wildcard type arguments are only supported for VECTOR"); } + ) + ) + { + if (nestedType != null) { + argumentsStringList.add(nestedType.toString()); + nestedType = null; + } else { + argumentsStringList.add((numericArgument != null ? numericArgument : tk.image) + + (tk2 != null ? " " + tk2.image : "")); } - ")" - ] + } + + [ "," ] + )* + ")" + ] + ( LOOKAHEAD(1) typeModifier=MySqlTypeModifier() + { colDataType.addTypeModifier(typeModifier); } )* + ( + LOOKAHEAD(1, ) { tk = null; } + [ LOOKAHEAD(2) "[" tk= "]" ] { + array.add(tk == null ? null : Integer.valueOf(tk.image)); + colDataType.setArrayData(array); + colDataType.setUsingArrayKeyword(true); + } + | + [ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk= ] + { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ + { colDataType.setArrayData(array); } ] ) + [ + LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET) + || isKeywordAhead("CHARSET") }) TypeCharacterSet(colDataType) + | + LOOKAHEAD({ Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) + && (isKeywordAhead("ASCII") || isKeywordAhead("UNICODE")) }) + tk= + { colDataType.setCharacterSetSyntax( + ColDataType.CharacterSetSyntax.valueOf(tk.image.toUpperCase(Locale.ROOT))); } + ] { - return result; + requireDdlSyntax(colDataType.getXmlTypeModifier() == null || argumentsStringList.isEmpty(), + "Typed XML cannot have additional type arguments"); + if (argumentsStringList.size() > 0) { + colDataType.setArgumentsStringList(argumentsStringList); + // Integer arguments expose numeric parameters, including negative scales. + if (argumentsStringList.size() == 1 && argumentsStringList.get(0).matches("\\d+")) { + colDataType.setNumericPrecision(new java.math.BigInteger(argumentsStringList.get(0))); + } else if (argumentsStringList.size() == 2 && argumentsStringList.get(0).matches("\\d+") + && argumentsStringList.get(1).matches("-?\\d+")) { + colDataType.setNumericPrecision(new java.math.BigInteger(argumentsStringList.get(0))); + colDataType.setScale(Integer.valueOf(argumentsStringList.get(1))); + } + } + if (zonePrecision != null) { + colDataType.setPrecision(zonePrecision); + } + linkAST(colDataType, first); + return colDataType; } } -IntervalExpression IntervalExpression() : { - IntervalExpression interval = new IntervalExpression(); - Token token = null; - Expression expr = null; - boolean signed = false; - IntervalQualifier qualifier = null; +/** Shared by column definitions, casts and other uses of ColDataType. */ +void TypeCharacterSet(ColDataType type): +{ + Token name; + boolean abbreviated = false; } { + ( + | LOOKAHEAD({ isKeywordAhead("CHARSET") }) { abbreviated = true; } ) + name=CharacterSetName() + { type.setCharacterSet(name.image); type.setUseCharsetKeyword(abbreviated); } +} - - ( +/** A MySQL character set or collation name: an identifier, BINARY or a quoted name. */ +Token CharacterSetName(): +{ Token name; } +{ + ( name= | name= | name= | name= ) + { return name; } +} - LOOKAHEAD(3) ( - [ "-" {signed=true;}] - (token= | token=) - ) - | - LOOKAHEAD(2) token= - | - expr = Expression() - ) - { - if (expr != null) { - if (signed) expr = new SignedExpression('-', expr); - interval.setExpression(expr); - } else { - interval.setParameter((signed?"-":"") + token.image); - } - } - [ - // Standard qualifier (field [TO field] [(precision)]) such as DAY or DAY TO SECOND. - LOOKAHEAD(2, { getToken(1).kind == K_DATE_LITERAL }) qualifier = IntervalQualifier() { interval.setIntervalQualifier(qualifier); } - // Non-standard single identifier field (e.g. MySQL `INTERVAL 1 foo`), kept for - // backwards compatibility. - | LOOKAHEAD(2) (token = ) { interval.setIntervalType(token.image); } +XmlTypeModifier XmlTypeModifier(): +{ + XmlTypeModifier.Kind kind = null; + List names = new ArrayList(); + String name; + Token token; +} +{ + "(" + [ LOOKAHEAD({ (isKeywordAhead("CONTENT") || isKeywordAhead("DOCUMENT")) + && !".".equals(getToken(2).image) && !")".equals(getToken(2).image) }) + token= { kind = XmlTypeModifier.Kind.valueOf(token.image.toUpperCase(Locale.ROOT)); } ] - { - return interval; - } + name=RelObjectName() { names.add(name); } + [ "." name=RelObjectNameExt() { names.add(name); } ] + ")" + { return new XmlTypeModifier(kind, names); } } -IntervalExpression IntervalExpressionWithoutInterval(Expression expr) : { - IntervalExpression interval; - IntervalQualifier qualifier = null; - boolean signed = false; +ColDataType.TypeModifier MySqlTypeModifier(): +{ + ColDataType.TypeModifier modifier; } { - { - interval = new IntervalExpression(false); - interval.setExpression(expr); - } - qualifier = IntervalQualifier() - { - interval.setIntervalQualifier(qualifier); - return interval; - } + ( + { modifier = ColDataType.TypeModifier.SIGNED; } + | { modifier = ColDataType.TypeModifier.UNSIGNED; } + | { modifier = ColDataType.TypeModifier.ZEROFILL; } + ) + { return modifier; } } /** - * Parses an SQL interval qualifier, e.g. {@code DAY}, {@code DAY TO SECOND}, - * {@code DAY(9) TO SECOND} or {@code SECOND(2, 4)}, as defined by the SQL standard. - * Shared by {@link IntervalExpression} (DML interval literals) and by {@code ColDataType} - * (DDL {@code INTERVAL} column / cast target types). + * Parses MySQL/MariaDB CHECK, CHECKSUM, OPTIMIZE and REPAIR TABLE statements. Options are accepted + * only for the operation that defines them, without reserving the option words. */ -IntervalQualifier IntervalQualifier() : { - Token field = null; - Token toField = null; - Token leadingPrecision = null; - Token fractionalPrecision = null; - Token trailingFractionalPrecision = null; +TableMaintenanceStatement TableMaintenanceStatement(): +{ + TableMaintenanceStatement statement = new TableMaintenanceStatement(); + TableMaintenanceStatement.Operation operation = null; + TableMaintenanceStatement.Option option = null; + Token modifier = null; + Token word = null; + Table table = null; } { - field = + ( + { operation = TableMaintenanceStatement.Operation.CHECK; } + | { operation = TableMaintenanceStatement.Operation.CHECKSUM; } + | { operation = TableMaintenanceStatement.Operation.OPTIMIZE; } + | { operation = TableMaintenanceStatement.Operation.REPAIR; } + ) + { statement.setOperation(operation); } [ - LOOKAHEAD(2) "(" leadingPrecision = - [ "," fractionalPrecision = ] - ")" + LOOKAHEAD({ operation.allowsModifier() + && (getToken(1).kind == K_NO_WRITE_TO_BINLOG || getToken(1).kind == K_LOCAL) }) + ( modifier= | modifier= ) + { + statement.setModifier(TableMaintenanceStatement.Modifier.valueOf( + modifier.image.toUpperCase(Locale.ROOT))); + } + ] + ( | { statement.setTablesKeyword(true); } ) + table=Table() { statement.addTables(table); } + ( "," table=Table() { statement.addTables(table); } )* + ( + LOOKAHEAD({ isTableMaintenanceOptionAhead(operation) }) + ( + ContextualKeyword("UPGRADE") + { option = TableMaintenanceStatement.Option.FOR_UPGRADE; } + | { option = TableMaintenanceStatement.Option.QUICK; } + | { option = TableMaintenanceStatement.Option.EXTENDED; } + | word= { option = TableMaintenanceStatement.Option.fromKeyword(word.image); } + ) + { statement.addOptions(option); } + )* + { + requireDdlSyntax(operation != TableMaintenanceStatement.Operation.CHECKSUM + || statement.getOptions().size() <= 1, "CHECKSUM TABLE takes QUICK or EXTENDED, not both"); + return statement; + } +} + +Analyze Analyze(): +{ + Analyze analyze = new Analyze(); + Table table = null; + Column column = null; + Token modifier = null; + Token histogramAction = null; + Token bucketCount = null; + Token data; +} +{ + + [ + LOOKAHEAD(2) + (modifier= | modifier=) + { analyze.setModifier(Analyze.Modifier.valueOf(modifier.image.toUpperCase(Locale.ROOT))); } ] + [ LOOKAHEAD(2) { analyze.setTableKeyword(true); } ] + table=Table() { analyze.addTables(table); } + ( "," table=Table() { analyze.addTables(table); } )* [ - LOOKAHEAD(2) toField = - [ LOOKAHEAD(2) "(" trailingFractionalPrecision = ")" ] + LOOKAHEAD(2) + (histogramAction= | histogramAction=) + { + analyze.setHistogramAction(Analyze.HistogramAction.valueOf( + histogramAction.image.toUpperCase(Locale.ROOT))); + } + + column=Column() { analyze.addHistogramColumns(column); } + ( "," column=Column() { analyze.addHistogramColumns(column); } )* + ( + // MySQL 8.4 sets a histogram from JSON instead of sampling the column + LOOKAHEAD(2) data= { + requireDdlSyntax(histogramAction.kind == K_UPDATE, "USING DATA requires UPDATE HISTOGRAM"); + analyze.setHistogramData(stringValue(data)); + } + | + [ + LOOKAHEAD(3) + bucketCount= + { analyze.setHistogramBucketCount(Integer.valueOf(bucketCount.image)); } + ] + [ + LOOKAHEAD({ (getToken(1).kind == K_AUTO || isKeywordAhead("MANUAL")) + && getToken(2).kind == K_UPDATE }) + ( { analyze.setHistogramUpdateMode(Analyze.HistogramUpdateMode.AUTO); } + | ContextualKeyword("MANUAL") { analyze.setHistogramUpdateMode(Analyze.HistogramUpdateMode.MANUAL); } ) + { + requireDdlSyntax(histogramAction.kind == K_UPDATE, "AUTO UPDATE and MANUAL UPDATE require UPDATE HISTOGRAM"); + } + ] + ) ] + { - // SECOND(2, 4) TO SECOND(3) specifies fractional seconds precision for both the leading - // and the trailing field. Reject such contradictory input instead of silently dropping one - // of the values. The rejection is placed last so the production does not complete normally - // and JavaCC does not append an unreachable "missing return" guard. - if (fractionalPrecision == null || trailingFractionalPrecision == null) { - return new IntervalQualifier( - field != null ? field.image : null, - leadingPrecision != null ? Integer.valueOf(leadingPrecision.image) : null, - toField != null ? toField.image : null, - trailingFractionalPrecision != null - ? Integer.valueOf(trailingFractionalPrecision.image) - : (fractionalPrecision != null - ? Integer.valueOf(fractionalPrecision.image) : null)); - } - throw new ParseException( - "Invalid interval qualifier: fractional seconds precision is specified for both" - + " the leading field and the trailing field"); + return analyze; } } -KeepExpression KeepExpression() : { - KeepExpression keep = new KeepExpression(); - Token token; - List list; +/** Parses PostgreSQL REINDEX; only DATABASE and SYSTEM may omit the name. */ +Reindex Reindex(): +{ + Reindex statement = new Reindex(); + Reindex.Option option; + Table table; + String name; } { - "(" token = ( { keep.setFirst(true); } | { keep.setFirst(false); }) - list = OrderByElements() ")" + + [ "(" option=ReindexOption() { statement.addOptions(option); } + ( "," option=ReindexOption() { statement.addOptions(option); } )* ")" ] + ( + ( { statement.setTarget(Reindex.Target.INDEX); } + | { statement.setTarget(Reindex.Target.TABLE); } ) + ReindexConcurrently(statement) + table=Table() { statement.setName(table); } + | + { statement.setTarget(Reindex.Target.SCHEMA); } + ReindexConcurrently(statement) + name=RelObjectName() { statement.setName(new Table(name, false)); } + | + ( { statement.setTarget(Reindex.Target.DATABASE); } + | { statement.setTarget(Reindex.Target.SYSTEM); } ) + ReindexConcurrently(statement) + [ LOOKAHEAD(1) name=RelObjectName() { statement.setName(new Table(name, false)); } ] + ) + { return statement; } +} + +void ReindexConcurrently(Reindex statement): +{} +{ + // CONCURRENTLY is a keyword here, as in CREATE INDEX, never the reindexed object's name + [ LOOKAHEAD({ getToken(1).kind == K_CONCURRENTLY }) { statement.setConcurrently(true); } ] +} +Reindex.Option ReindexOption(): +{ Reindex.Option option = new Reindex.Option(); Token token; String value = null; } +{ + ( { option.setName(Reindex.Option.Name.CONCURRENTLY); } + | { option.setName(Reindex.Option.Name.TABLESPACE); } + | { option.setName(Reindex.Option.Name.VERBOSE); } ) + [ + LOOKAHEAD(1) + ( token= | token= | token= | token= | token= ) + { value = token.image; } + | value=RelObjectName() + ] { - keep.setName(token.image); - keep.setOrderByElements(list); - return keep; + requireDdlSyntax(option.getName() != Reindex.Option.Name.TABLESPACE || value != null, + "The REINDEX TABLESPACE option requires a tablespace name"); + option.setValue(value); + return option; } } - -void windowFun(AnalyticExpression retval):{ - ExpressionList expressionList = null; - boolean partitionByBrackets = false; - String windowName = null; - WindowDefinition winDef; -} { +/** + * Parses PostgreSQL SECURITY LABEL. The object kind selects how the object is addressed: by a + * name, a qualified column, a routine signature or a large object OID. + */ +SecurityLabel SecurityLabel(): +{ + SecurityLabel statement = new SecurityLabel(); + SecurityLabel.ObjectType type; + Token token; + String name; + Table object; + Column column; + RoutineReference routine; + StringValue label; +} +{ + ContextualKeyword("LABEL") + [ ( token= { statement.setProvider(token.image); } + | name=RelObjectName() { statement.setProvider(name); } ) ] + type=SecurityLabelObjectType(statement) ( - {retval.setType(AnalyticType.OVER);} + LOOKAHEAD({ type == SecurityLabel.ObjectType.COLUMN }) + column=Column() { statement.setColumn(column); } | - {retval.setType(AnalyticType.WITHIN_GROUP);} + LOOKAHEAD({ type.isRoutine() }) + routine=RoutineReference(type == SecurityLabel.ObjectType.AGGREGATE) + { statement.setRoutine(type, routine); } + | + LOOKAHEAD({ type == SecurityLabel.ObjectType.LARGE_OBJECT }) + token= { statement.setLargeObjectOid(Long.parseLong(token.image)); } + | + LOOKAHEAD({ type.isRelation() || type == SecurityLabel.ObjectType.SEQUENCE + || type == SecurityLabel.ObjectType.TYPE || type == SecurityLabel.ObjectType.DOMAIN }) + object=CommentObjectName() { statement.setObjectName(type, object); } + | + name=RelObjectName() { statement.setObjectName(type, new Table(name, false)); } ) + ( label=CharacterLiteral(true) { statement.setLabel(label); } | ) + { return statement; } +} +SecurityLabel.ObjectType SecurityLabelObjectType(SecurityLabel statement): +{ SecurityLabel.ObjectType type; } +{ ( - windowName = RelObjectName() { retval.setWindowName(windowName); } - | - winDef = windowDefinition() { retval.setWindowDefinition(winDef); } - - [ - LOOKAHEAD(2) "(" - [ - (LOOKAHEAD(3) expressionList=ComplexExpressionList() - | "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" ) - ] - ")" { winDef.setPartitionExpressionList(expressionList, partitionByBrackets); retval.setType(AnalyticType.WITHIN_GROUP_OVER); } - ] + { type = SecurityLabel.ObjectType.TABLE; } + | { type = SecurityLabel.ObjectType.COLUMN; } + | { type = SecurityLabel.ObjectType.AGGREGATE; } + | { type = SecurityLabel.ObjectType.DATABASE; } + | { type = SecurityLabel.ObjectType.DOMAIN; } + | { type = SecurityLabel.ObjectType.EVENT_TRIGGER; } + | { type = SecurityLabel.ObjectType.FOREIGN_TABLE; } + | { type = SecurityLabel.ObjectType.FUNCTION; } + | { type = SecurityLabel.ObjectType.MATERIALIZED_VIEW; } + | { type = SecurityLabel.ObjectType.PROCEDURE; } + | { type = SecurityLabel.ObjectType.SCHEMA; } + | { type = SecurityLabel.ObjectType.SEQUENCE; } + | { type = SecurityLabel.ObjectType.TABLESPACE; } + | { type = SecurityLabel.ObjectType.TYPE; } + | { type = SecurityLabel.ObjectType.VIEW; } + | LOOKAHEAD({ isKeywordAhead("LARGE") }) ContextualKeyword("LARGE") ContextualKeyword("OBJECT") + { type = SecurityLabel.ObjectType.LARGE_OBJECT; } + | LOOKAHEAD({ isKeywordAhead("PROCEDURAL") }) ContextualKeyword("PROCEDURAL") + ContextualKeyword("LANGUAGE") { type = SecurityLabel.ObjectType.LANGUAGE; statement.setProcedural(true); } + | LOOKAHEAD({ isKeywordAhead("LANGUAGE") }) ContextualKeyword("LANGUAGE") + { type = SecurityLabel.ObjectType.LANGUAGE; } + | LOOKAHEAD({ isKeywordAhead("PUBLICATION") }) ContextualKeyword("PUBLICATION") + { type = SecurityLabel.ObjectType.PUBLICATION; } + | LOOKAHEAD({ isKeywordAhead("ROLE") }) ContextualKeyword("ROLE") + { type = SecurityLabel.ObjectType.ROLE; } + | LOOKAHEAD({ isKeywordAhead("ROUTINE") }) ContextualKeyword("ROUTINE") + { type = SecurityLabel.ObjectType.ROUTINE; } + | LOOKAHEAD({ isKeywordAhead("SUBSCRIPTION") }) ContextualKeyword("SUBSCRIPTION") + { type = SecurityLabel.ObjectType.SUBSCRIPTION; } ) + { return type; } } -WindowDefinition windowDefinition() : { - ExpressionList expressionList = null; - List olist = null; - WindowElement windowElement = null; - boolean partitionByBrackets = false; - WindowDefinition winDef = new WindowDefinition(); -} { +ExpressionList ColumnWithCommentList(): +{ + ExpressionList expressions = new ExpressionList(); + Column img = null; +} +{ "(" - [ - (LOOKAHEAD(3) expressionList=ComplexExpressionList() - | "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" ) + img=Column() { expressions.add(img); } + ( "," img=Column() { expressions.add(img); } )* + ")" + { + return linkElements(expressions); + } +} + + +CreateView CreateView(boolean isUsingOrReplace): +{ + CreateView createView = new CreateView(); + Table view = null; + Select select = null; + ExpressionList columnNames = null; + Token tk = null; + List commentTokens = null; + String name = null; + List storageParameters = null; + List viewOptions = new ArrayList(); + ViewOption viewOption = null; + boolean withData; + MySqlViewOptions mySqlOptions; + CreateView.CheckOption checkOption; +} +{ + { createView.setOrReplace(isUsingOrReplace);} + mySqlOptions=MySqlViewPrefix() { createView.setMySqlOptions(mySqlOptions); } + [ + { createView.setForce(ForceOption.NO_FORCE); } + | { createView.setForce(ForceOption.FORCE); } ] - [olist=OrderByElements() ] - [windowElement = WindowElement() ] - { - winDef.setPartitionExpressionList(expressionList, partitionByBrackets); - winDef.setOrderByElements(olist); - winDef.setWindowElement(windowElement); - } - ")" - { return winDef; } + [ { createView.setSecure(true);} ] + [ + { createView.setTemporary(TemporaryOption.TEMP); } + | { createView.setTemporary(TemporaryOption.TEMPORARY); } + | { createView.setTemporary(TemporaryOption.VOLATILE); } + ] + [ { createView.setRecursive(true); } ] + [ { createView.setMaterialized(true);} ] + + [ LOOKAHEAD(3) { createView.setIfNotExists(true); } ] + view=Table() { createView.setView(view); } + [ LOOKAHEAD({ isKeywordAhead("BACKUP") }) tk= + (tk= | tk=) { createView.setBackup(tk.kind == K_YES); } ] + [LOOKAHEAD(3) (tk= | tk=) { createView.setAutoRefresh(AutoRefreshOption.from(tk.image)); } ] + [LOOKAHEAD(3) { + createView.setIfNotExists(true); + createView.setIfNotExistsAfterViewName(true); + }] + [ columnNames=ColumnWithCommentList( ) { createView.setColumnNames(columnNames); } ] + [ commentTokens=CreateViewTailComment( ) { createView.setViewCommentOptions(commentTokens); } ] + [ name=RelObjectName() { createView.setAccessMethod(name); } ] + [ + ( + LOOKAHEAD({ createView.isMaterialized() }) + storageParameters=PostgreSqlIndexOptions() { createView.setStorageParameters(storageParameters); } + | + "(" viewOption=PostgreSqlViewOption() { viewOptions.add(viewOption); } + ( "," viewOption=PostgreSqlViewOption() { viewOptions.add(viewOption); } )* ")" + { createView.setOptions(viewOptions); } + ) + ] + [ name=RelObjectName() { createView.setTableSpace(name); } ] + + select=Select( ) { createView.setSelect(select); } + [ LOOKAHEAD(2) + ( + { createView.setWithReadOnly(true); } + | + checkOption=ViewCheckOption() { createView.setCheckOption(checkOption); } + | + withData=DataPopulation() { createView.setWithData(withData); } + ) + ] + { createView.validateOptions(); return createView; } } -AnalyticExpression AnalyticExpression(Function function) : +CreateView.CheckOption ViewCheckOption(): { - AnalyticExpression retval = new AnalyticExpression(function); - Expression filter = null; + CreateView.CheckOption option = CreateView.CheckOption.DEFAULT; } { - ( - ( - "(" {retval.setType(AnalyticType.FILTER_ONLY);} filter = Expression() ")" - [ LOOKAHEAD(2) windowFun(retval) ] - ) - | windowFun(retval) - ) - { - retval.setFilterExpression(filter); - return retval; - } + [ { option = CreateView.CheckOption.LOCAL; } + | ContextualKeyword("CASCADED") { option = CreateView.CheckOption.CASCADED; } ] + + { return option; } } -WindowElement WindowElement(): +ViewOption PostgreSqlViewOption(): { - WindowElement windowElement = new WindowElement(); - WindowRange range = new WindowRange(); - WindowOffset offset = null; - WindowElement.Exclusion exclusion = null; + String name; + Expression value = null; + boolean useEquals = false; } { - ( { windowElement.setType(WindowElement.Type.ROWS); } - | { windowElement.setType(WindowElement.Type.RANGE); } - | { windowElement.setType(WindowElement.Type.GROUPS); }) - ( ( - { windowElement.setRange(range); } - offset = WindowOffset() { range.setStart(offset); } - offset = WindowOffset() { range.setEnd(offset); } - ) - | - offset = WindowOffset() { windowElement.setOffset(offset); } - ) - [ exclusion = FrameExclusion() { windowElement.setExclusion(exclusion); } ] + name=RelObjectName() + [ "=" { useEquals = true; } value=PrimaryExpression() ] + { return new ViewOption(name, value, useEquals); } +} - { - return windowElement; - } +/** Shared WITH [NO] DATA tail for table and materialized-view query sources. */ +boolean DataPopulation(): +{ boolean withData = true; } +{ + [ { withData = false; } ] + { return withData; } } -WindowElement.Exclusion FrameExclusion(): +List CreateViewTailComment(): { - WindowElement.Exclusion exclusion = null; + Token tk = null; + Token tk2 = null; + String op = null; + List result = new ArrayList(); } { - - ( - - { exclusion = WindowElement.Exclusion.CURRENT_ROW; } - | - - { exclusion = WindowElement.Exclusion.GROUP; } - | - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("TIES") - }) - - { exclusion = WindowElement.Exclusion.TIES; } - | - - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("OTHERS") - }) - - { exclusion = WindowElement.Exclusion.NO_OTHERS; } - ) - { return exclusion; } + tk= + [ "=" { op = "="; } ] + tk2 = { + result.add(""); + result.add(tk.image); + if (op != null) { + result.add(op); + } + result.add(tk2.image); + } + { return result;} } -WindowOffset WindowOffset(): + +ReferentialAction.Action Action(): { - WindowOffset offset = new WindowOffset(); - Expression expr = null; + ReferentialAction.Action action = null; } { - ( - ( - ( { offset.setType(WindowOffset.Type.PRECEDING); } | - { offset.setType(WindowOffset.Type.FOLLOWING); } ) - ) - | - LOOKAHEAD(2) ( { offset.setType(WindowOffset.Type.CURRENT); } ) - | - ( expr = SimpleExpression() { - offset.setType(WindowOffset.Type.EXPR); - offset.setExpression(expr); - } - ( { offset.setType(WindowOffset.Type.PRECEDING); } | { offset.setType(WindowOffset.Type.FOLLOWING); } ) - ) - ) + ( { action=ReferentialAction.Action.CASCADE;} + | + {action=ReferentialAction.Action.RESTRICT;} + | + {action=ReferentialAction.Action.NO_ACTION;} + | + ( + {action=ReferentialAction.Action.SET_NULL;} + | + {action=ReferentialAction.Action.SET_DEFAULT;} + ) + ) + { return action; } +} - { - return offset; - } +/** + * Parses optional referential actions: [ON DELETE|UPDATE action] [ON DELETE|UPDATE action] + * Shared between CREATE TABLE FK and ALTER TABLE FK definitions. + */ +void ReferentialActions(ForeignKeyReference reference): +{} +{ + [ LOOKAHEAD(2) ReferentialActionSpec(reference) ] + [ LOOKAHEAD(2) ReferentialActionSpec(reference) ] } -ExtractExpression ExtractExpression() : +void ReferentialActionSpec(ForeignKeyReference reference): { - ExtractExpression retval = new ExtractExpression(); - String fieldName = null; - Token token = null; - Expression expr = null; + Token token; + ReferentialAction.Type type; + ReferentialAction.Action action; + List columnNames = null; } { - - "(" - ( fieldName=RelObjectName() { retval.setName(fieldName); } | token= { retval.setName(token.image); } ) - - expr=SimpleExpression() { retval.setExpression(expr); } - ")" + ( token= | token= ) + { type = ReferentialAction.Type.from(token.image); } + action=Action() + [ LOOKAHEAD({ type == ReferentialAction.Type.DELETE + && (action == ReferentialAction.Action.SET_NULL + || action == ReferentialAction.Action.SET_DEFAULT) + && "(".equals(getToken(1).image) }) + columnNames=ColumnsNamesList() ] { - return retval; + reference.setReferentialAction(type, action); + reference.getReferentialAction(type).setColumnNames(columnNames); } } -CastExpression ImplicitCast() #ImplicitCast: +/** Shares column/period boundaries on both sides of a foreign key. */ +List ForeignKeyColumns(boolean allowParameters): { - ColDataType colDataType; - Token tk1; - Token tk2; - - int precision = -1; - int scale = -1; + List columns = new ArrayList(); + KeyElement column; } { - colDataType = DataType() - ( - tk2 = - | - tk2 = - | - tk2 = - ) - { - CastExpression castExpression = new CastExpression(colDataType, tk2.image); - return castExpression; - } + "(" column=ForeignKeyColumn(allowParameters) { columns.add(column); } + ( "," { + requireDdlSyntax(!column.isPeriod(), "PERIOD must mark the final foreign key column"); + } column=ForeignKeyColumn(allowParameters) { columns.add(column); } )* ")" + { return columns; } } -CastExpression CastExpression(): +KeyElement ForeignKeyColumn(boolean allowParameters): { - Token keyword; - CastExpression retval; - ColumnDefinition columnDefinition; - ColDataType type; - Expression expression = null; - boolean useCastKeyword; - Token formatCharLiteral; + boolean period = false; + String name; + List parameters = null; } { - ( - keyword= - | - keyword= - | - keyword= - | - keyword= - ) - { - retval = new CastExpression(keyword.image); - } - "(" - expression=SimpleExpression() - { retval.setUseCastKeyword(true); } - ( - LOOKAHEAD(2) ( - - "(" - columnDefinition=ColumnDefinition() { retval.addColumnDefinition(columnDefinition); } - ( "," columnDefinition=ColumnDefinition() { retval.addColumnDefinition(columnDefinition); } )* - ")" - ) - | - type=ColDataType() { retval.setColDataType(type); } - ) - - // BigQuery FORMAT clause - // https://cloud.google.com/bigquery/docs/reference/standard-sql/conversion_functions#cast_as_date - [ formatCharLiteral= { retval.setFormat(formatCharLiteral.image); } ] - - ")" - - { - retval.setLeftExpression(expression); - return retval; - } + [ LOOKAHEAD({ isKeywordAhead("PERIOD") && getToken(2).kind != K_COMMA + && getToken(2).kind != CLOSING_BRACKET }) + ContextualKeyword("PERIOD") { period = true; } ] + name=RelObjectName() + [ parameters=CreateParameter() { + requireDdlSyntax(allowParameters && !period, "Expected a foreign key column name"); + } ] + { return new KeyElement(name, parameters).withPeriod(period); } } -Expression CaseWhenExpression() #CaseWhenExpression: +ForeignKeyReference ForeignKeyReferenceSpec(boolean columnContext): { - CaseExpression caseExp = new CaseExpression(); - Expression switchExp = null; - WhenClause clause; - List whenClauses = new ArrayList(); - Expression elseExp = null; + ForeignKeyReference reference = new ForeignKeyReference(); + ForeignKeyReference.MatchType matchType; + Token matchToken; + List refColNames = null; + List referencedColumns; + Table fkTable; } { - { caseCounter++; } - [ LOOKAHEAD(2) switchExp=Expression() ] - ( clause=WhenThenSearchCondition() { whenClauses.add(clause); } )+ + fkTable=Table() + [ LOOKAHEAD(2) referencedColumns=ForeignKeyColumns(false) { + refColNames = new ArrayList(); + for (KeyElement column : referencedColumns) { + refColNames.add(column.getColumnName()); + } + reference.setUsingPeriod(referencedColumns.get(referencedColumns.size() - 1).isPeriod()); + } ] + { + reference.setTable(fkTable); + reference.setReferencedColumnNames(refColNames); + } [ - + ( - LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted}) elseExp=Expression() - | elseExp=SimpleExpression() + { matchType = ForeignKeyReference.MatchType.FULL; } + | matchToken= { + matchType = ForeignKeyReference.MatchType.valueOf( + matchToken.image.toUpperCase(Locale.ROOT)); + } ) - ] - { caseCounter--; } - { - caseExp.setSwitchExpression(switchExp); - caseExp.setWhenClauses(whenClauses); - caseExp.setElseExpression(elseExp); - return caseExp; + { reference.setMatchType(matchType); } + ] + ReferentialActions(reference) + { + if (columnContext) { + ConstraintAttributes attributes = PostgreSqlConstraintAttributeList(null); + requireDdlSyntax(attributes == null || !attributes.isNotValid(), + "NOT VALID requires a table constraint"); + reference.setConstraintAttributes(attributes); + } + return reference; } } -WhenClause WhenThenSearchCondition(): +/** + * Parses: CHECK ( expression ) + * Returns a CheckConstraint. Shared between CREATE TABLE and ALTER TABLE. + */ +CheckConstraint CheckConstraintSpec(String constraintName): { - WhenClause whenThen = new WhenClause(); - Expression whenExp; - Expression thenExp; + Expression exp = null; + Boolean enforced = null; + boolean noInherit = false; + CheckConstraint checkConstraint; } { - whenExp=Expression() - - ( - LOOKAHEAD({getAsBoolean(Feature.allowComplexParsing) && !interrupted}) thenExp=Expression() - | - thenExp=SimpleExpression() - ) + "(" exp = Expression() ")" + [ LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) }) + TypeDdlKeyword("INHERIT") { noInherit = true; } ] + [ LOOKAHEAD(2) enforced=ConstraintEnforcement() ] { - whenThen.setWhenExpression(whenExp); - whenThen.setThenExpression(thenExp); - return whenThen; + checkConstraint = new CheckConstraint().withName(constraintName).withExpression(exp) + .withEnforced(enforced).withNoInherit(noInherit); + checkConstraint.setKind(ConstraintKind.CHECK); + return checkConstraint; } } -RowConstructor RowConstructor(): { - Token token; - ParenthesedExpressionList expressions; -} { - token= - expressions = ParenthesedExpressionList() +/** + * Parses: FOREIGN KEY columns REFERENCES table [columns] [referential actions] + * Returns a populated ForeignKeyConstraint. Shared between CREATE TABLE and ALTER TABLE. + */ +ForeignKeyConstraint ForeignKeySpec(String constraintName): +{ + ForeignKeyConstraint fkIndex = new ForeignKeyConstraint(); + String indexName = null; + Token tk; + Token tk2; + List colNames; + ForeignKeyReference reference; +} +{ + tk= tk2= + [ LOOKAHEAD(2) indexName=RelObjectName() { fkIndex.setIndexName(indexName); } ] + colNames = ForeignKeyColumns(true) { - return new RowConstructor(token.image, expressions); + if (constraintName != null) { fkIndex.setName(constraintName); } + fkIndex.withType(tk.image + " " + tk2.image).withColumns(colNames); } -} - -/** -TODO: VariableExpression should be a standalone class with more operations available. -*/ -EqualsTo VariableExpression(): { - Expression left; - Expression right; -} { - left = UserVariable() "=" right = SimpleExpression() + reference=ForeignKeyReferenceSpec(false) { fkIndex.setReference(reference); } { - EqualsTo equals = new EqualsTo(); - equals.setLeftExpression(left); - equals.setRightExpression(right); - return equals; + return fkIndex; } } -Execute Execute(): { - Token token; - ObjectNames funcName; - ExpressionList expressionList = null; - Execute execute = new Execute(); - List namedExprList; - Expression expr; +/** + * Parses USING [INDEX] [name] [TABLESPACE name] clause in ALTER TABLE constraint tails. + */ +void AlterExpressionUsingIndex(AlterExpression alterExp): +{ + String sk4; } { - ( { execute.setExecType(Execute.ExecType.EXEC); } - | { execute.setExecType(Execute.ExecType.EXECUTE); } - | { execute.setExecType(Execute.ExecType.CALL); } ) - - funcName=RelObjectNames() { execute.setName(funcName.getNames()); } - + { alterExp.addParameters("USING"); } + [ LOOKAHEAD(2) { alterExp.addParameters("INDEX"); } ] ( - LOOKAHEAD(2) expressionList=ExpressionList() { execute.setExprList(expressionList); } - )? + LOOKAHEAD(1, ) sk4=RelObjectName() + { alterExp.addParameters("TABLESPACE"); alterExp.addParameters(sk4); } + | + sk4=RelObjectName() { alterExp.addParameters(sk4); } + [ LOOKAHEAD(2) sk4=RelObjectName() + { alterExp.addParameters("TABLESPACE"); alterExp.addParameters(sk4); } ] + ) +} - { - return execute; - } +/** + * Parses the common tail after a named constraint in ALTER TABLE: + * constraint_state [USING [INDEX] name] [COMMENT 'text'] + */ +void AlterExpressionConstraintTail(AlterExpression alterExp, KeyConstraint constraint): +{ + List constraints; +} +{ + constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } + [ LOOKAHEAD(2) AlterExpressionUsingIndex(alterExp) ] + [ LOOKAHEAD(2) IndexWithComment(constraint) ] } -FullTextSearch FullTextSearch() : { - Token searchModifier; - Expression againstValue; - FullTextSearch fs = new FullTextSearch(); - ExpressionList matchedColumns; +AlterView AlterView(boolean useReplace): +{ + AlterView alterView = new AlterView(); + MySqlViewOptions options; + CreateView.CheckOption checkOption; + Table view = null; + Select select = null; + List columnNames = null; } { - "(" matchedColumns=ColumnList() ")" - "(" - againstValue=SimpleExpression() { fs.setAgainstValue(againstValue); } - [ - ( - searchModifier="IN NATURAL LANGUAGE MODE" - | searchModifier="IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION" - | searchModifier="IN BOOLEAN MODE" - | searchModifier="WITH QUERY EXPANSION" - ) - { fs.setSearchModifier(searchModifier.image); } - ] - ")" + options=MySqlViewPrefix() { alterView.setMySqlOptions(options); } + view=Table() { alterView.setView(view); alterView.setUseReplace(useReplace); } + [ columnNames = ColumnsNamesList() { alterView.setColumnNames(columnNames); } ] + + select=Select() + [ LOOKAHEAD(2) checkOption=ViewCheckOption() { alterView.setCheckOption(checkOption); } ] { - fs.setMatchColumns(matchedColumns); - return fs; + alterView.setSelect(select); + return alterView; } } -LambdaExpression LambdaExpression() #LambdaExpression: +List CreateParameter(): { - ExpressionList columns; - String s; - Expression expression; - LambdaExpression lambdaExpression; + String retval = ""; + Token tk = null, tk2 = null; + Expression exp = null; + ColDataType colDataType; + List param = new ArrayList(); } { ( - columns = ParenthesedColumnList() + // Postgres: nextval('public.actor_actor_id_seq'::regclass) + ( "(" tk= "::" colDataType = ColDataType() ")" ) + { + param.add("NextVal( " + tk.image + "::" + colDataType + ")" ); + } + | + ( + //@todo: implement a proper identifier + (tk= | tk= | tk= | tk=) + { retval+=tk.image; } + + [ + "." + //@todo: implement a proper identifier + (tk2= | tk2= | tk=) + { retval+="."+tk2.image; } + ] + { param.add(retval); } + ) + | + LOOKAHEAD(3) [ { param.add("USING INDEX"); }] retval=RelObjectName() + { param.add("TABLESPACE " + retval); } + | + ( + tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk = | tk = + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= + // MySQL index_option / algorithm_option / lock_option keywords, e.g. the trailing + // "KEY_BLOCK_SIZE = 8 ALGORITHM = INPLACE LOCK = NONE" of CREATE INDEX, the + // "WITH PARSER" option, and the FULLTEXT / SPATIAL index types of CREATE INDEX. + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= + | tk="=" + ) + { param.add(tk.image); } + | + ( tk= | tk= | tk= ) [ LOOKAHEAD(2) "(" exp = Expression() ")" ] + { + param.add(tk.image); + if (exp!=null) { + param.add("(" + exp + ")"); + } + } + | + ( + [ ( tk="+" | tk="-" ) { retval = tk.image; } ] + tk= | tk= + ) + { param.add( retval + tk.image ); } + | + retval=AList() { param.add(retval); } + | + (tk= tk2=) { param.add(tk.image); param.add(tk2.image);} + | + ( exp=ArrayConstructor(true)) { param.add(exp.toString()); } | - s = RelObjectName() { columns = new ExpressionList(new Column(s)); } + tk="::" colDataType = ColDataType() { param.add(tk.image); param.add(colDataType.toString()); } ) + {return param;} +} - "->" - expression = Expression() +// row_movement_clause https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_7002.htm#i2204697 +RowMovement RowMovement(): +{ + RowMovement rowMovement = new RowMovement(); +} +{ + ( + ( ) { rowMovement.setMode(RowMovementMode.ENABLE); } + | + ( ) { rowMovement.setMode(RowMovementMode.DISABLE); } + ) + { return rowMovement;} +} + +String AList(): +{ + StringBuilder retval = new StringBuilder("("); + Token tk = null; + String name = null; + boolean needsSeparator = false; +} +{ + "(" + + ( + ( (tk= | tk= | tk= | tk= | tk=) { name = tk.image; } + | name=RelObjectName() ) + { + if (needsSeparator) { + retval.append(' '); + } + retval.append(name); + needsSeparator = true; + } + [("," { retval.append(','); needsSeparator = false; } + | "=" { retval.append('='); needsSeparator = false; })] )* + + ")" { - lambdaExpression = LambdaExpression.from(columns, expression); - linkAST(lambdaExpression,jjtThis); - return lambdaExpression; + retval.append(")"); + return retval.toString(); } } -Function Function() #Function: +String ColumnsNamesListItem(): { - Function function; + Token tk = null; + Token sortDirection = null; + String item = null; } { - ( - "{" function = InternalFunction(true) "}" - | LOOKAHEAD(3) function = SpecialStringFunctionWithNamedParameters() - | function = InternalFunction(false) - ) + ( item = RelObjectName() ) + [ LOOKAHEAD(2) "(" tk = ")" { item = item + "(" + tk.image + ")"; } ] + [ LOOKAHEAD( ( | ) ( "," | ")" ) ) (sortDirection = | sortDirection = ) { item = item + " " + sortDirection.image; } ] + { + return item; + } +} + +List ColumnsNamesList(): +{ + List retval = new ArrayList(); + String img = null; +} +{ + "(" + img=ColumnsNamesListItem() { retval.add(img); } + ( "," img=ColumnsNamesListItem() { retval.add(img); } )* + + ")" { - linkAST(function,jjtThis); - return function; + return retval; } } -Function SpecialStringFunctionWithNamedParameters() : +/** DROP names are lexer-separated identifiers, not strings to split again on dots. */ +Table DropObjectName() #TableName: { - Token funcName; - NamedExpressionList namedExpressionList = null; - ExpressionList expressionList = null; - List orderByList; + ObjectNames names; + Token literal; + Table table; + String timeTravel = null; } { - funcName = - - "(" - ( - LOOKAHEAD( { isNamedExprListAhead() } ) namedExpressionList = NamedExpressionListExprFirst() - | - expressionList=ExpressionList() - ) - ")" - - { - return new Function() - .withName(funcName.image) - .withNamedParameters(namedExpressionList) - .withParameters(expressionList); - } + ( + names=RelObjectNames() [ LOOKAHEAD(2) timeTravel=TimeTravelBeforeAlias() ] + { table = names.getNames().size() == 1 ? new Table(names.getNames().get(0), false) + : new Table(names.getNames()); table.setTimeTravel(timeTravel); } + | + literal= { table = new Table(literal.image, false); } + ) + { linkAST(table, jjtThis); return table; } } -JAVACODE -List consumeKeywordArguments() { - List args = null; - while (isKeywordArgumentAhead()) { - String name = getNextToken().image; - ExpressionList exprList = SimpleExpressionList(); - if (args == null) { - args = new ArrayList(); +/** The targets of a DROP statement, typed by the object kind that was dropped. */ +void DropTargets(Drop drop): +{ + boolean aggregate = drop.getObjectType() == Drop.ObjectType.AGGREGATE; + RoutineReference routine; + UserAccount account; + Table name; +} +{ + ( + LOOKAHEAD({ drop.getObjectType().hasRoutineTargets() }) + routine=RoutineReference(aggregate) { drop.addRoutines(routine); } + ( "," routine=RoutineReference(aggregate) { drop.addRoutines(routine); } )* + | + LOOKAHEAD({ drop.getObjectType().hasAccountTargets() }) + account=MySqlAccountName() { drop.addAccounts(account); } + ( "," account=MySqlAccountName() { drop.addAccounts(account); } )* + { + if (drop.getObjectType() == Drop.ObjectType.ROLE) { + requireRoleNames(drop.getAccounts()); + } } - Expression value = exprList.size() == 1 - ? (Expression) exprList.get(0) - : exprList; - args.add(new Function.KeywordArgument(name, value)); - } - return args; + | + name=DropObjectName() { drop.addNames(name); } + ( "," name=DropObjectName() { drop.addNames(name); } )* + ) } -Function InternalFunction(boolean escaped): +Drop Drop(): { - Token prefixToken = null; - Function retval = new Function(); - ObjectNames funcName; - ExpressionList expressionList = null; - ExpressionList chainedExpressionList = null; - KeepExpression keep = null; - Expression expr = null; - Expression attributeExpression = null; - Column attributeColumn = null; - Token attributeToken = null; - Limit limit; - List keywordArgs = null; + Drop drop = new Drop(); + Token tk = null; + Table name; + Table indexTable = null; + int indexTablePosition = 0; + List dropArgs = new ArrayList(); + AlgorithmOrLockOption indexOption; + List indexOptions = new ArrayList(); + int indexOptionPosition = 0; + boolean useTemporary = false; + String multiWordType = null; + boolean eventTrigger = false; + CascadeBehavior behavior = null; + String engine; + String accessMethod; } { - [ LOOKAHEAD(2) prefixToken = ] - funcName = RelObjectNames() { if (prefixToken!=null) funcName.getNames().add(0, prefixToken.image ); } - - "(" - [ - LOOKAHEAD(2) [ - LOOKAHEAD(2) ( - { retval.setDistinct(true); } - | { retval.setAllColumns(true); } - | { retval.setUnique(true); } - ) - ] - ( - LOOKAHEAD(3) expressionList = FunctionArgumentList(retval) - - | - // The probe above is syntactic and cannot see through the Pratt operator - // loop, so it fails on arguments such as "( 1 + a ) / ( 1 + b )" and used - // to fall through to Select(), reporting "Encountered ... - // was expecting ". Select() is only reachable here for a bare - // sub-select, so decide on that instead. - LOOKAHEAD({ !isUnparenthesizedSelectAhead() }) expressionList = FunctionArgumentList(retval) - - | - LOOKAHEAD({ !getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) expr = Select() { expressionList = new ExpressionList(expr); } - ) - ] - - [ - ( - expr = Expression() { retval.setHavingClause( "MIN", expr ); } + + [ { drop.setMaterialized(true);} ] + ( + ( + { multiWordType = "FOREIGN TABLE"; } | - expr = Expression() { retval.setHavingClause( "MAX", expr ); } + ContextualKeyword("WRAPPER") { multiWordType = "FOREIGN DATA WRAPPER"; } ) - ] - - [ + | + // PostgreSQL object types of several words, written as COMMENT ON names them + LOOKAHEAD({ isKeywordAhead("ACCESS") && "METHOD".equalsIgnoreCase(getToken(2).image) }) + ContextualKeyword("ACCESS") ContextualKeyword("METHOD") { multiWordType = "ACCESS METHOD"; } + | + LOOKAHEAD({ isKeywordAhead("PROCEDURAL") && "LANGUAGE".equalsIgnoreCase(getToken(2).image) }) + ContextualKeyword("PROCEDURAL") ContextualKeyword("LANGUAGE") + { drop.setProcedural(true); multiWordType = "LANGUAGE"; } + | + // a schema named CLASS or FAMILY may qualify an operator, as in class.+ + LOOKAHEAD({ isKeywordAhead("OPERATOR") && !".".equals(getToken(3).image) + && ("CLASS".equalsIgnoreCase(getToken(2).image) || "FAMILY".equalsIgnoreCase(getToken(2).image)) }) + ContextualKeyword("OPERATOR") + ( LOOKAHEAD({ isKeywordAhead("CLASS") }) ContextualKeyword("CLASS") { multiWordType = "OPERATOR CLASS"; } + | ContextualKeyword("FAMILY") { multiWordType = "OPERATOR FAMILY"; } ) + | + LOOKAHEAD({ isKeywordAhead("TEXT") && getToken(2).kind == K_SEARCH }) ( - { retval.setNullHandling(Function.NullHandling.IGNORE_NULLS); } + { multiWordType = "TEXT SEARCH PARSER"; } + | LOOKAHEAD({ isKeywordAhead("CONFIGURATION") }) ContextualKeyword("CONFIGURATION") + { multiWordType = "TEXT SEARCH CONFIGURATION"; } + | LOOKAHEAD({ isKeywordAhead("DICTIONARY") }) ContextualKeyword("DICTIONARY") + { multiWordType = "TEXT SEARCH DICTIONARY"; } + | ContextualKeyword("TEMPLATE") { multiWordType = "TEXT SEARCH TEMPLATE"; } ) | + // PostgreSQL DROP OWNED BY role [, ...] + LOOKAHEAD({ isKeywordAhead("OWNED") && getToken(2).kind == K_BY }) tk= + | + // MySQL DROP UNDO TABLESPACE + LOOKAHEAD({ isKeywordAhead("UNDO") && getToken(2).kind == K_TABLESPACE }) + tk= { drop.setUndo(true); } + | + tk= + | ( - { retval.setNullHandling(Function.NullHandling.RESPECT_NULLS); } + [ {useTemporary=true;} ] tk= ) - ] + | + tk= + | + tk= + | + tk= + | + tk= + | + tk= + | + [ { drop.setPublicSynonym(true); } ] tk= + | + tk= + | + tk= + | + tk= + | + tk= + | + // PostgreSQL DROP EVENT TRIGGER and MySQL DROP EVENT (TRIGGER cannot name a MySQL event) + tk= [ LOOKAHEAD(1) { eventTrigger = true; } ] + | + tk= + | + tk= + | + tk= + | + tk= + | + tk= + ) + { drop.setType(multiWordType != null ? multiWordType : eventTrigger ? "EVENT TRIGGER" : tk.image); } + [ LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.INDEX + && getToken(1).kind == K_CONCURRENTLY }) + { drop.setConcurrently(true); } ] + [ LOOKAHEAD(2, { drop.getObjectType() != Drop.ObjectType.OWNED }) + {drop.setIfExists(true);} ] + + DropTargets(drop) [ - limit = PlainLimit() { retval.setLimit(limit); } + // PostgreSQL DROP OPERATOR CLASS and FAMILY require USING: they name one object of an + // index access method. The semantic lookahead alone selects this clause. + LOOKAHEAD({ drop.getObjectType().hasAccessMethod() }) + accessMethod=RelObjectName() { + requireDdlSyntax(drop.getNames().size() == 1, "DROP " + drop.getType() + " takes one name"); + drop.setAccessMethod(accessMethod); + } ] - - { - keywordArgs = consumeKeywordArguments(); - } - - ")" - [ - LOOKAHEAD(2) "(" chainedExpressionList = ExpressionList() ")" + LOOKAHEAD({ DropLookahead.isForceAhead(drop, getToken(1), getToken(2), getToken(3)) }) + ( | [ ] "(" ")" ) { drop.setForce(true); } ] - - [ - LOOKAHEAD(2) - ( - LOOKAHEAD({ getToken(1).kind == S_DOUBLE - && getToken(1).image.matches("\\.[0-9]+") }) - attributeToken= { - retval.setAttribute(new LongValue(attributeToken.image.substring(1))); - } - | - "." ( - // tricky lookahead since we do need to support the following constructs - // schema.f1().f2() - Function with Function Column - // schema.f1().f2.f3 - Function with Attribute Column - LOOKAHEAD( 6 ) attributeExpression=Function() { retval.setAttribute(attributeExpression); } - | - attributeColumn=Column() { retval.setAttribute(attributeColumn); } - ) - ) + // MySQL DROP [UNDO] TABLESPACE name [ ENGINE [=] engine_name ] + LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.TABLESPACE && getToken(1).kind == K_ENGINE }) + [ "=" { drop.setEngineUseEquals(true); } ] engine=RelObjectName() { drop.setEngine(engine); } ] - - [ LOOKAHEAD(2) ( - ( - - { - retval.setNullHandling(Function.NullHandling.IGNORE_NULLS); - retval.setIgnoreNullsOutside(true); + ( + // MySQL DROP INDEX accepts a trailing algorithm_option / lock_option, e.g. + // "DROP INDEX i ON t ALGORITHM = INPLACE LOCK = NONE". LOCK also starts a LOCK TABLE + // statement, so it is only taken here when it cannot begin the next statement. + LOOKAHEAD({ getToken(1).kind == K_ALGORITHM + || (getToken(1).kind == K_LOCK && getToken(2).kind != K_TABLE) }) + indexOption=IndexAlgorithmOrLockOption() { + if (indexOptions.isEmpty()) { + indexOptionPosition = dropArgs.size(); + } + indexOptions.add(indexOption); + } + | + tk= { dropArgs.add(tk.image); } + | + ( + name = DropObjectName() { + if (DropLookahead.isOwnerTable(drop, name)) { + requireNoDropOwnerTable(indexTable); + indexTable = name; + indexTablePosition = dropArgs.size(); + } else { + dropArgs.add("ON"); dropArgs.add(name.toString()); } - ) - | - ( - - { - retval.setNullHandling(Function.NullHandling.RESPECT_NULLS); - retval.setIgnoreNullsOutside(true); - } - ) + } ) - ] - - [ LOOKAHEAD(2) keep = KeepExpression() ] - + )* + [ { behavior = CascadeBehavior.CASCADE; } | { behavior = CascadeBehavior.RESTRICT; } ] { - retval.setEscaped(escaped); - retval.setParameters(expressionList); - retval.setChainedParameters(chainedExpressionList); - retval.setName(funcName.getNames()); - retval.setKeep(keep); - retval.setKeywordArguments(keywordArgs); - return retval; - } -} - -/** - * Argument list of a Function, extracted from InternalFunction() so that it can be - * reached from more than one lookahead guard without duplicating the expansion. - */ -ExpressionList FunctionArgumentList(Function retval): -{ - ExpressionList expressionList = null; - List orderByList; - String onOverflowTruncate = null; - Token overflowToken = null; - Token extraKeywordToken; -} -{ - [ LOOKAHEAD(2) extraKeywordToken = { retval.setExtraKeyword(extraKeywordToken.image); } ] - expressionList=ExpressionList() - [ orderByList = OrderByElements() { retval.setOrderByElements(orderByList); } ] - - // https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/LISTAGG.html - [ - ( overflowToken= | overflowToken= ) { onOverflowTruncate=overflowToken.image; } - [ - overflowToken = { onOverflowTruncate+= " " + overflowToken.image; } - [ ( overflowToken= | overflowToken= ) { onOverflowTruncate+=" " + overflowToken.image + " COUNT"; }] - ] - ] { retval.setOnOverflowTruncate(onOverflowTruncate); } + if (dropArgs.size() > 0) { + drop.setParameters(dropArgs); + } + if (indexTable != null) { + drop.setTable(indexTable, indexTablePosition); + } + if (!indexOptions.isEmpty()) { + drop.setAlgorithmOrLockOptions(indexOptions, indexOptionPosition); + } + drop.setBehavior(behavior); - { - return expressionList; - } -} + drop.setUsingTemporary(useTemporary); -XMLSerializeExpr XMLSerializeExpr(): { - XMLSerializeExpr result; - Expression expression; - List orderByElements = null; - ColDataType dataType; -} -{ - - "(" - "(" - "(" expression=SimpleExpression() ")" - [ orderByElements=OrderByElements() ] - ")" - dataType=ColDataType() ")" - { - result = new XMLSerializeExpr(); - result.setExpression(expression); - result.setOrderByElements(orderByElements); - result.setDataType(dataType); - return result; + return drop; } } +/** PostgreSQL REASSIGN OWNED BY role [, ...] TO role */ +ReassignOwned ReassignOwned(): +{ + ReassignOwned statement = new ReassignOwned(); + UserAccount role; +} +{ + ContextualKeyword("OWNED") + role=MySqlAccountName() { statement.addRoles(role); } + ( "," role=MySqlAccountName() { statement.addRoles(role); } )* + role=MySqlAccountName() { statement.setNewRole(role); } + { return statement; } +} -JsonTableFunction.JsonTablePassingClause JsonTablePassingClause() : { - Expression valueExpression; - String parameterName; +Truncate Truncate(): +{ + Truncate truncate = new Truncate(); + TableReference target; + String log = null, storage = null, identity = null, behavior = null; } { - valueExpression = Expression() - - parameterName = RelObjectName() + + [ LOOKAHEAD(2) { truncate.setTableToken(true); } ] + target=RelationTarget() { truncate.getTargets().add(target); } + ( LOOKAHEAD(2) "," target=RelationTarget() { truncate.getTargets().add(target); } )* + [ + LOOKAHEAD({ isKeywordAhead("PRESERVE") || getToken(1).kind == K_PURGE }) + ( ContextualKeyword("PRESERVE") { log = "PRESERVE"; } + | { log = "PURGE"; } ) + + ] + [ + LOOKAHEAD({ getToken(1).kind == K_DROP || isKeywordAhead("REUSE") }) + ( { storage = "DROP"; } + [ { storage = "DROP_ALL"; } ] + | ContextualKeyword("REUSE") { storage = "REUSE"; } ) + + ] + [ ( { identity = "RESTART"; } + | ContextualKeyword("CONTINUE") { identity = "CONTINUE"; } ) + ] + [ { behavior = "CASCADE"; } + | { behavior = "RESTRICT"; } ] { - return new JsonTableFunction.JsonTablePassingClause(valueExpression, parameterName); + RelationOptions.apply(truncate, log, storage, identity, behavior); + return truncate; } } -JsonFunction.JsonOnResponseBehavior JsonTableOnEmptyBehavior() : { - JsonFunction.JsonOnResponseBehavior behavior = null; - Expression expression; - Token token; +/** + * Parses common column-level changes shared between the COLUMN-prefixed and bare forms: + * DROP DEFAULT, SET DEFAULT, SET NOT NULL, SET VISIBLE/INVISIBLE, and bracketed multi-column + * definitions. + */ +void AlterExpressionColumnChanges(AlterExpression alterExp): +{ + AlterExpression.ColumnDataType alterExpressionColumnDataType = null; + AlterExpression.ColumnDropDefault alterExpressionColumnDropDefault = null; + AlterExpression.ColumnSetDefault alterExpressionColumnSetDefault = null; + AlterExpression.ColumnSetNotNull alterExpressionColumnSetNotNull = null; + AlterExpression.ColumnSetVisibility alterExpressionColumnSetVisibility = null; } { ( - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.ERROR); - } - | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.NULL); - } + LOOKAHEAD(3) alterExpressionColumnDropDefault = AlterExpressionColumnDropDefault() + { alterExp.addColDropDefault(alterExpressionColumnDropDefault); } | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.TRUE); - } + LOOKAHEAD(3) alterExpressionColumnSetDefault = AlterExpressionColumnSetDefault() + { alterExp.addColSetDefault(alterExpressionColumnSetDefault); } | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.FALSE); - } + LOOKAHEAD(4) alterExpressionColumnSetNotNull = AlterExpressionColumnSetNotNull() + { alterExp.addColSetNotNull(alterExpressionColumnSetNotNull); } | - expression = Expression() - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.DEFAULT, expression); - } + LOOKAHEAD(3) alterExpressionColumnSetVisibility = AlterExpressionColumnSetVisibility() + { alterExp.addColSetVisibility(alterExpressionColumnSetVisibility); } | - token = - { - if (!token.image.equalsIgnoreCase("EMPTY")) { - throw new ParseException( - "Expected EMPTY, ERROR, NULL or DEFAULT but found " + token.image); - } - } - ( - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("OBJECT") }) - JsonKeyword("OBJECT") - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY_OBJECT); - } - | - [ ] - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY); - } + LOOKAHEAD(4) ( + "(" + { alterExp.useBrackets(true);} + alterExpressionColumnDataType = AlterExpressionColumnDataType() { + alterExp.addColDataType(alterExpressionColumnDataType); + } + ( + "," + alterExpressionColumnDataType = AlterExpressionColumnDataType() { + alterExp.addColDataType(alterExpressionColumnDataType); + } + )* + ")" ) ) - { - if (behavior != null) { - return behavior; - } - } } -JsonTableFunction.JsonTableWrapperClause JsonTableWrapperClause(boolean beforePathExpr) : { - JsonTableFunction.JsonTableWrapperClause wrapperClause = - new JsonTableFunction.JsonTableWrapperClause(beforePathExpr); - Token token; + +AlterExpression.ColumnDataType AlterExpressionColumnDataType(): +{ + String columnName = null; + boolean withType = false; + boolean useSetData = false; + ColDataType dataType = null; + List columnOptions = new ArrayList(); + ColumnOption option = null; + AlterExpression.ColumnDataType result; + Expression usingExpression = null; + List identityAlterations = null; + IdentityAlteration identityAlteration; + ColumnPosition position = null; } { + columnName = RelObjectName() ( - { - wrapperClause.setWrapperType(JsonFunction.JsonWrapperType.WITHOUT); - } - | - { - wrapperClause.setWrapperType(JsonFunction.JsonWrapperType.WITH); - } - [ - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && (getToken(1).image.equalsIgnoreCase("CONDITIONAL") - || getToken(1).image.equalsIgnoreCase("UNCONDITIONAL")) - }) - token = - { - if (token.image.equalsIgnoreCase("CONDITIONAL")) { - wrapperClause.setWrapperMode(JsonFunction.JsonWrapperMode.CONDITIONAL); - } else { - wrapperClause.setWrapperMode(JsonFunction.JsonWrapperMode.UNCONDITIONAL); - } - } - ] + LOOKAHEAD({ isIdentityAlterationAhead() }) + { identityAlterations = new ArrayList(); } + identityAlteration=ColumnIdentityAlteration() { identityAlterations.add(identityAlteration); } + ( LOOKAHEAD({ isIdentityAlterationAhead() }) + identityAlteration=ColumnIdentityAlteration() { identityAlterations.add(identityAlteration); } )* + | + [ LOOKAHEAD({ getToken(1).kind == K_TYPE || getToken(1).kind == K_SET && getToken(2).kind == K_DATA }) + useSetData=TypeChangePrefix() { withType = true; } ] + ( LOOKAHEAD(2) dataType = ColDataType() )? + ( LOOKAHEAD(2, { !(withType && getToken(1).kind == K_USING) + && getToken(1).kind != K_FIRST && !isKeywordAhead("AFTER") + && !isTrailingPartitionOptionAhead() }) + option = ColumnDefinitionOption() { + columnOptions.add(option); + } )* + [ LOOKAHEAD({ withType && getToken(1).kind == K_USING }) usingExpression=Expression() ] + [ LOOKAHEAD({ getToken(1).kind == K_FIRST || isKeywordAhead("AFTER") }) + position = ColumnPosition() ] ) - [ { wrapperClause.setArray(true); } ] - JsonKeyword("WRAPPER") { - return wrapperClause; + requireDdlSyntax(!withType || dataType != null, "TYPE requires a data type"); + result = new AlterExpression.ColumnDataType(columnName, withType, dataType, null); + result.setUseSetData(useSetData); + result.setUsingExpression(usingExpression); + result.setIdentityAlterations(identityAlterations); + result.setColumnOptions(columnOptions); + result.setPosition(position); + return result; } } -JsonTableFunction.JsonTableQuotesClause JsonTableQuotesClause() : { - JsonTableFunction.JsonTableQuotesClause quotesClause = - new JsonTableFunction.JsonTableQuotesClause(); +/** MySQL places an ADD, CHANGE or MODIFY column definition with a trailing FIRST or AFTER col. */ +ColumnPosition ColumnPosition(): +{ + ColumnPosition position; + String columnName; } { ( - { quotesClause.setQuotesType(JsonFunction.JsonQuotesType.KEEP); } + { position = ColumnPosition.first(); } | - JsonKeyword("OMIT") { quotesClause.setQuotesType(JsonFunction.JsonQuotesType.OMIT); } + ContextualKeyword("AFTER") columnName = RelObjectName() + { position = ColumnPosition.after(columnName); } ) - JsonKeyword("QUOTES") - [ - JsonKeyword("SCALAR") { quotesClause.setOnScalarString(true); } - ] - { - return quotesClause; - } + { return position; } } -JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { - JsonTableFunction.JsonTableColumnDefinition columnDefinition = null; - JsonTableFunction.JsonTableNestedColumnDefinition nestedColumnDefinition; - JsonTableFunction.JsonTableValueColumnDefinition valueColumnDefinition; - String columnName; - ColDataType dataType; - Expression expression; - String pathName = null; - JsonTableFunction.JsonTableColumnsClause columnsClause; - JsonFunction.JsonOnResponseBehavior behavior; - JsonTableFunction.JsonTableWrapperClause wrapperClause; - JsonTableFunction.JsonTableQuotesClause quotesClause; - String encoding; +/** Type changes in ALTER TABLE and ALTER TYPE accept the same optional SET DATA prefix. */ +boolean TypeChangePrefix(): +{ boolean useSetData = false; } +{ + [ { useSetData = true; } ] + { return useSetData; } +} + +IdentityAlteration ColumnIdentityAlteration(): +{ + IdentityAlteration alteration; + IdentityDefinition identity; + IdentityDefinition.GenerationMode mode; + Sequence.Parameter parameter; + Long restart = null; + Token token; } { ( - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("NESTED") }) - JsonKeyword("NESTED") - { nestedColumnDefinition = new JsonTableFunction.JsonTableNestedColumnDefinition(); } - [ LOOKAHEAD(2) { nestedColumnDefinition.setPathKeyword(true); } ] - expression = Expression() { nestedColumnDefinition.setPathExpression(expression); } - [ pathName = RelObjectName() { nestedColumnDefinition.setPathName(pathName); } ] - columnsClause = JsonTableColumnsClause() { - nestedColumnDefinition.setColumnsClause(columnsClause); - columnDefinition = nestedColumnDefinition; + identity=IdentityDefinition() { + alteration = new IdentityAlteration(IdentityAlteration.Kind.ADD); + alteration.setIdentityDefinition(identity); } | - columnName = RelObjectName() { - valueColumnDefinition = new JsonTableFunction.JsonTableValueColumnDefinition(); - valueColumnDefinition.setColumnName(columnName); - columnDefinition = valueColumnDefinition; - } + ( - { valueColumnDefinition.setForOrdinality(true); } + LOOKAHEAD({ isKeywordAhead("GENERATED") }) token= + ( { mode = IdentityDefinition.GenerationMode.ALWAYS; } + | { mode = IdentityDefinition.GenerationMode.BY_DEFAULT; } ) + { + alteration = new IdentityAlteration(IdentityAlteration.Kind.SET_GENERATED); + alteration.setGenerationMode(mode); + } | - [ - // Very ugly: ColDataType can consume an IDENTIFIER, which is fine, but we don't want it to - // consume an ALLOW or DISALLOW because that's a keyword for Oracle in this place. - // So we make a LOOKAHEAD on ColDataType, BUT we exclude the two cases for the IDENTIFIER - - /* - [ ColDataType ] - [ FORMAT JSON ] - [ (ALLOW | DISALLOW) SCALARS ] - */ - - LOOKAHEAD( - ColDataType(), - { !(getToken(1).kind == S_IDENTIFIER && ( - getToken(1).image.equalsIgnoreCase("ALLOW") - || getToken(1).image.equalsIgnoreCase("DISALLOW"))) } ) - dataType = ColDataType() { valueColumnDefinition.setDataType(dataType); } - ] - [ - { valueColumnDefinition.setFormatJson(true); } - [ encoding = JsonEncoding() { valueColumnDefinition.setEncoding(encoding); } ] - ] - [ - ( - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("ALLOW") }) - JsonKeyword("ALLOW") { valueColumnDefinition.setScalarsType(JsonFunction.ScalarsType.ALLOW); } - | - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("DISALLOW") }) - JsonKeyword("DISALLOW") { valueColumnDefinition.setScalarsType(JsonFunction.ScalarsType.DISALLOW); } - ) - JsonKeyword("SCALARS") - ] - [ { valueColumnDefinition.setExistsKeyword(true); } ] - // In Oracle, the wrapper clause comes before the PATH expression - [ LOOKAHEAD(2) wrapperClause = JsonTableWrapperClause(true) { valueColumnDefinition.setWrapperClause(wrapperClause); } ] - [ expression = Expression() { valueColumnDefinition.setPathExpression(expression); } ] - // In Truno the wrapper clause comes after the PATH expression - [ wrapperClause = JsonTableWrapperClause(false) { valueColumnDefinition.setWrapperClause(wrapperClause); } ] - [ - LOOKAHEAD({ - getToken(1).kind == K_KEEP - || (getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("OMIT")) - }) - quotesClause = JsonTableQuotesClause() { valueColumnDefinition.setQuotesClause(quotesClause); } - ] - [ - LOOKAHEAD( JsonTableOnEmptyBehavior() ) - behavior = JsonTableOnEmptyBehavior() - - { valueColumnDefinition.setOnEmptyBehavior(behavior); } - ] - [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) - behavior = JsonQueryOnResponseBehavior() - - { valueColumnDefinition.setOnErrorBehavior(behavior); } - ] - [ - LOOKAHEAD( JsonTableOnEmptyBehavior() ) - behavior = JsonTableOnEmptyBehavior() - - { - valueColumnDefinition.setOnEmptyBehavior(behavior); - valueColumnDefinition.setOnEmptyAfterOnError(true); - } - ] + parameter=SequenceParameter() { + alteration = new IdentityAlteration(IdentityAlteration.Kind.SET_PARAMETER); + alteration.setParameters(Collections.singletonList(parameter)); + } ) + | + [ LOOKAHEAD(1) [ ] restart=SequenceParameterValue() ] { + alteration = new IdentityAlteration(IdentityAlteration.Kind.RESTART); + alteration.setRestartWith(restart); + } + | + { alteration = new IdentityAlteration(IdentityAlteration.Kind.DROP); } + [ LOOKAHEAD(2) { alteration.setIfExists(true); } ] ) + { return alteration; } +} + +AlterExpression.ColumnDropNotNull AlterExpressionColumnDropNotNull(): +{ + String columnName = null; + boolean withNot = false; + ColDataType dataType = null; + List columnSpecs = null; + List parameter = null; +} +{ + columnName = RelObjectName() + + ( { withNot = true; } )? + + { + return new AlterExpression.ColumnDropNotNull(columnName, withNot); + } +} + +AlterExpression.ColumnSetNotNull AlterExpressionColumnSetNotNull(): +{ + String columnName = null; +} +{ + columnName = RelObjectName() + + { - return columnDefinition; + return new AlterExpression.ColumnSetNotNull(columnName); } } -JsonTableFunction.JsonTableColumnsClause JsonTableColumnsClause() : { - JsonTableFunction.JsonTableColumnsClause columnsClause = - new JsonTableFunction.JsonTableColumnsClause(); - JsonTableFunction.JsonTableColumnDefinition columnDefinition; +AlterExpression.ColumnDropDefault AlterExpressionColumnDropDefault(): +{ + String columnName = null; + boolean withNot = false; + ColDataType dataType = null; + List columnSpecs = null; + List parameter = null; } { - "(" - [ - columnDefinition = JsonTableColumnDefinition() { - columnsClause.addColumnDefinition(columnDefinition); - } - ( - "," - columnDefinition = JsonTableColumnDefinition() { - columnsClause.addColumnDefinition(columnDefinition); - } - )* - ] - ")" + columnName = RelObjectName() { - return columnsClause; + return new AlterExpression.ColumnDropDefault(columnName); } } -JsonTableFunction.JsonTablePlanTerm JsonTablePlanTerm() : { - JsonTableFunction.JsonTablePlanTerm term = null; - String value; - Expression expression; - JsonTableFunction.JsonTablePlanExpression nestedPlanExpression; +AlterExpression.ColumnSetDefault AlterExpressionColumnSetDefault(): +{ + String columnName = null; + Expression defaultValue = null; +} +{ + columnName = RelObjectName() defaultValue = Expression() + { + return AlterExpression.ColumnSetDefault.fromExpression(columnName, defaultValue); + } +} + +AlterExpression.ColumnSetVisibility AlterExpressionColumnSetVisibility(): +{ + String columnName = null; + boolean visible = true; } { + columnName = RelObjectName() ( - LOOKAHEAD(2) - "(" nestedPlanExpression = JsonTablePlanExpression() ")" { - term = new JsonTableFunction.JsonTablePlanTerm(); - term.setNestedPlanExpression(nestedPlanExpression); - } - | - LOOKAHEAD(2) - value = RelObjectName() { - term = new JsonTableFunction.JsonTablePlanTerm(); - term.setName(value); - } - | - expression = Expression() { - term = new JsonTableFunction.JsonTablePlanTerm(); - term.setExpression(expression); - } + { visible = true; } | + { visible = false; } ) { - return term; + return new AlterExpression.ColumnSetVisibility(columnName, visible); } } -JsonTableFunction.JsonTablePlanExpression JsonTablePlanExpression() : { - JsonTableFunction.JsonTablePlanExpression planExpression = - new JsonTableFunction.JsonTablePlanExpression(); - JsonTableFunction.JsonTablePlanTerm term; - Token operator = null; +List AlterExpressionConstraintState(): +{ + List retval = new ArrayList(); } { - term = JsonTablePlanTerm() { planExpression.addTerm(term); } ( ( - operator = { - planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.COMMA); - } - | - operator = { - planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.INNER); - } - | - operator = { - planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.OUTER); - } - | - operator = { - planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.CROSS); - } - | - operator = { - planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.UNION); - } + {retval.add(new DeferrableConstraint(false));} + ) + | + ( + {retval.add(new DeferrableConstraint(true));} + ) + | + ( + {retval.add(new ValidateConstraint(false));} + ) + | + ( + {retval.add(new ValidateConstraint(true));} + ) + | + ( + {retval.add(new EnableConstraint(false));} + ) + | + ( + {retval.add(new EnableConstraint(true));} ) - term = JsonTablePlanTerm() { planExpression.addTerm(term); } )* { - return planExpression; + return retval; } } -JsonTableFunction.JsonTablePlanClause JsonTablePlanClause() : { - JsonTableFunction.JsonTablePlanClause planClause = - new JsonTableFunction.JsonTablePlanClause(); - JsonTableFunction.JsonTablePlanExpression planExpression; +void IndexWithComment(IndexOptionSource index): +{ + Token tk = null; } { - - [ { planClause.setDefaultPlan(true); } ] - "(" planExpression = JsonTablePlanExpression() ")" { planClause.setPlanExpression(planExpression); } + tk= { + index.setCommentText(tk.image); + } +} + +String UsingIndexType() : +{ + String sk = null; +} +{ + ( sk = RelObjectName() ) { - return planClause; + return sk; } } -JsonTableFunction.JsonTableOnErrorClause JsonTableOnErrorClause(boolean beforeColumns) : { - JsonTableFunction.JsonTableOnErrorClause onErrorClause = - new JsonTableFunction.JsonTableOnErrorClause(beforeColumns); +/** MySQL's index_option, shared by CREATE INDEX and the key definitions of CREATE/ALTER TABLE. */ +IndexSpecOption IndexSpecOption(): +{ Token token; + String value; + TableOption scalar; + IndexSpecOption option; } { ( - { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.ERROR); } + // KEY_BLOCK_SIZE and the engine attributes share the scalar syntax of table options. + LOOKAHEAD({ getToken(1).kind == K_KEY_BLOCK_SIZE || isMySqlEngineAttributeAhead() }) + scalar=MySqlScalarTableOption() { + option = IndexSpecOption.of(IndexSpecOption.Kind.valueOf(scalar.getKind().name()), + scalar.getValue(), scalar.isUseEquals()); + } | - { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.EMPTY); } + value=UsingIndexType() { option = IndexSpecOption.of(IndexSpecOption.Kind.INDEX_TYPE, value); } | - { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.TRUE); } + value=RelObjectName() + { option = IndexSpecOption.of(IndexSpecOption.Kind.PARSER, value); } | - { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.FALSE); } + token= { option = IndexSpecOption.comment(stringValue(token)); } | - { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.NULL); } + { option = IndexSpecOption.visibility(true); } + | + { option = IndexSpecOption.visibility(false); } ) - - { - return onErrorClause; - } + { return option; } } -JsonTableFunction.JsonTableOnEmptyClause JsonTableOnEmptyClause() : { - JsonTableFunction.JsonTableOnEmptyClause onEmptyClause = - new JsonTableFunction.JsonTableOnEmptyClause(); - Token token; +TablePartitioning CreateTablePartitioning(): +{ + TablePartitioning partitioning = null; + TablePartitioning subPartitioning = null; + Expression expression = null; + ExpressionList expressionList = null; + ExpressionList columns = null; + List partitionDefinitions = null; + List parameter = null; + List partitionOptions = new ArrayList(); + Token tk = null; } { + ( - { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.ERROR); } + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) }) + partitioning=PostgreSqlPartitionKey() | - { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.EMPTY); } + // explicit lookaheads: the method keywords win over the BigQuery + // bare-expression alternative below, which they share a prefix with + LOOKAHEAD(2) partitioning=HashOrKeyPartitioning() | - { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.TRUE); } + LOOKAHEAD(2) { partitioning = new TablePartitioning(TablePartitioning.Type.RANGE); } + ( + "(" expressionList=SimpleExpressionList() ")" { + if (expressionList.size() == 1) { + partitioning.setExpression(expressionList.get(0)); + } else { + partitioning.setExpressionList(expressionList); + } + } + | + "(" columns=ColumnList() ")" + { + partitioning.setColumnsSyntax(true); + partitioning.setColumns(columns); + } + ) | - { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.FALSE); } + LOOKAHEAD(2) { partitioning = new TablePartitioning(TablePartitioning.Type.LIST); } + ( + "(" expressionList=SimpleExpressionList() ")" { + if (expressionList.size() == 1) { + partitioning.setExpression(expressionList.get(0)); + } else { + partitioning.setExpressionList(expressionList); + } + } + | + "(" columns=ColumnList() ")" + { + partitioning.setColumnsSyntax(true); + partitioning.setColumns(columns); + } + ) | - { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.NULL); } + // BigQuery partitions by a bare expression: PARTITION BY d, PARTITION BY DATE(ts), + // PARTITION BY RANGE_BUCKET(id, GENERATE_ARRAY(0, 100, 10)) + LOOKAHEAD({ getToken(1).kind != K_HASH && getToken(1).kind != K_KEY + && getToken(1).kind != K_LINEAR && getToken(1).kind != K_RANGE + && getToken(1).kind != K_LIST }) + expression=SimpleExpression() + { + partitioning = new TablePartitioning(TablePartitioning.Type.EXPRESSION); + partitioning.setExpression(expression); + } ) - - { - return onEmptyClause; + [ LOOKAHEAD({ getToken(1).kind == K_PARTITIONS }) + tk= { partitioning.setPartitions(Long.valueOf(tk.image)); } ] + [ + LOOKAHEAD({ getToken(1).kind == K_SUBPARTITION }) + subPartitioning=HashOrKeyPartitioning() + [ LOOKAHEAD({ getToken(1).kind == K_SUBPARTITIONS }) + tk= + { subPartitioning.setPartitions(Long.valueOf(tk.image)); } ] + { partitioning.setSubPartitioning(subPartitioning); } + ] + [ LOOKAHEAD(2) partitionDefinitions=PartitionDefinitions() + { partitioning.setPartitionDefinitions(partitionDefinitions); } ] + ( LOOKAHEAD(2, { !isCreateTableQueryPrefix() + && !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) }) + parameter=CreateParameter() { partitionOptions.addAll(parameter); } )* + { + if (!partitionOptions.isEmpty()) { + partitioning.setPartitionOptions(partitionOptions); + } + return partitioning; } } -JsonTableFunction.JsonTableParsingTypeClause JsonTableParsingTypeClause() : { - JsonTableFunction.JsonTableParsingTypeClause parsingType = new JsonTableFunction.JsonTableParsingTypeClause(); +/** Shares collation/opclass nodes with index keys, excluding index-only ordering. */ +TablePartitioning PostgreSqlPartitionKey(): +{ + TablePartitioning result; + Token method; + List keys; + ExpressionList expressions = new ExpressionList(); + boolean attributes = false; } { - - - ( - { parsingType.setType(JsonTableFunction.JsonTableParsingType.STRICT); } - | - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("LAX") }) - JsonKeyword("LAX") { parsingType.setType(JsonTableFunction.JsonTableParsingType.LAX); } - ) - + ( method= | method= | method= ) + keys=IndexKeyColumns(false) { - return parsingType; + result = new TablePartitioning(TablePartitioning.Type.valueOf(method.image.toUpperCase(Locale.ROOT))); + for (KeyElement key : keys) { + requireDdlSyntax(key.getSortOrder() == null && key.getNullOrdering() == null + && key.getOperatorClassParameters() == null, + "Partition keys do not accept index ordering or operator-class parameters"); + attributes |= key.getCollation() != null || key.getOperatorClass() != null; + Expression expression = key.getExpression(); + if (expression == null) { expression = createColumn(key.getColumnName()); } + else if (key.isExpressionParenthesized()) { + expression = new ParenthesedExpressionList(expression); + } + expressions.add(expression); + } + if (attributes) { result.setKeyColumns(keys); } + else if (expressions.size() == 1) { result.setExpression(expressions.get(0)); } + else { result.setExpressionList(expressions); } + return result; } } -JsonTableFunction JsonTableBody() : { - JsonTableFunction function = new JsonTableFunction(); - Expression jsonInput; - Expression jsonPath; - JsonTableFunction.JsonTablePassingClause passingClause; - String pathName = null; - JsonTableFunction.JsonTableColumnsClause columnsClause; - JsonTableFunction.JsonTablePlanClause planClause = null; - JsonTableFunction.JsonTableOnErrorClause onErrorClause = null; - JsonTableFunction.JsonTableParsingTypeClause parsingTypeClause = null; - JsonTableFunction.JsonTableOnEmptyClause onEmptyClause = null; +PartitionBound PostgreSqlPartitionBound(): +{ + PartitionBound bound = null; + ExpressionList expressions = null; + Expression modulus = null; + Expression remainder = null; } { - "(" - jsonInput = Expression() { - function.setJsonInputExpression(jsonInput); - } - [ { function.setFormatJson(true); } ] - [ - "," - jsonPath = Expression() { - function.setJsonPathExpression(jsonPath); - function.setParameters(new ExpressionList(jsonInput, jsonPath)); - } - [ pathName = RelObjectName() { function.setPathName(pathName); } ] - ] - [ - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) - JsonKeyword("PASSING") - passingClause = JsonTablePassingClause() { function.addPassingClause(passingClause); } + ( + { bound = new PartitionBound(PartitionBound.Type.DEFAULT); } + | + ( - "," - passingClause = JsonTablePassingClause() { function.addPassingClause(passingClause); } - )* - ] - [ LOOKAHEAD(3) onErrorClause = JsonTableOnErrorClause(true) { function.setOnErrorClause(onErrorClause); } ] - [ parsingTypeClause = JsonTableParsingTypeClause() { function.setParsingTypeClause(parsingTypeClause); } ] - [ onEmptyClause = JsonTableOnEmptyClause() { function.setOnEmptyClause(onEmptyClause); } ] - columnsClause = JsonTableColumnsClause() { function.setColumnsClause(columnsClause); } - [ planClause = JsonTablePlanClause() { function.setPlanClause(planClause); } ] - [ onErrorClause = JsonTableOnErrorClause(false) { function.setOnErrorClause(onErrorClause); } ] - ")" - { - return function; - } + "(" expressions=SimpleExpressionList() ")" + { + bound = new PartitionBound(PartitionBound.Type.RANGE); + bound.setFromExpressions(expressions); + } + "(" expressions=SimpleExpressionList() ")" + { bound.setToExpressions(expressions); } + | + "(" expressions=SimpleExpressionList() ")" + { + bound = new PartitionBound(PartitionBound.Type.LIST); + bound.setInExpressions(expressions); + } + | + "(" modulus=Expression() "," + remainder=Expression() ")" + { + bound = new PartitionBound(PartitionBound.Type.HASH); + bound.setModulus(modulus); + bound.setRemainder(remainder); + } + ) + ) + { return bound; } } -XmlTableFunction.XmlTablePassingClause XmlTablePassingClause() : { - XmlTableFunction.XmlTablePassingClause passingClause = new XmlTableFunction.XmlTablePassingClause(); - Expression valueExpression; - String name = null; +TablePartitioning HashOrKeyPartitioning(): +{ + TablePartitioning partitioning = new TablePartitioning(); + ExpressionList expressionList = null; + ExpressionList columns = null; + Token tk = null; + boolean useEquals = false; } { - valueExpression = Expression() { passingClause.setValueExpression(valueExpression); } - [ name = RelObjectName() { passingClause.setName(name); } ] + [ { partitioning.setLinear(true); } ] + ( + "(" expressionList=SimpleExpressionList() ")" + { + partitioning.setType(TablePartitioning.Type.HASH); + if (expressionList.size() == 1) { + partitioning.setExpression(expressionList.get(0)); + } else { + partitioning.setExpressionList(expressionList); + } + } + | + + [ [ "=" { useEquals = true; } ] tk= + { + partitioning.setAlgorithm(Integer.valueOf(tk.image)); + partitioning.setAlgorithmUseEquals(useEquals); + } + ] + "(" [ columns=ColumnList() ] ")" + { + partitioning.setType(TablePartitioning.Type.KEY); + if (columns == null) { + columns = new ExpressionList(); + } + partitioning.setColumns(columns); + } + ) { - return passingClause; + return partitioning; } } -XmlTableFunction.XmlTableColumnDefinition XmlTableColumnDefinition() : { - XmlTableFunction.XmlTableColumnDefinition columnDefinition = new XmlTableFunction.XmlTableColumnDefinition(); - String columnName; - ColDataType dataType; - Expression expression; +List PartitionValues(): +{ + List values = new ArrayList(); + Expression expression = null; } { - columnName = RelObjectName() { columnDefinition.setColumnName(columnName); } ( - { columnDefinition.setForOrdinality(true); } - | - dataType = ColDataType() { columnDefinition.setDataType(dataType); } - [ expression = Expression() { columnDefinition.setPathExpression(expression); } ] - [ expression = Expression() { columnDefinition.setDefaultExpression(expression); } ] + LOOKAHEAD({ getToken(1).kind == K_MAXVALUE }) + { values.add("MAXVALUE"); } + | expression=Expression() { values.add(expression.toString()); } ) + ( + "," + ( + LOOKAHEAD({ getToken(1).kind == K_MAXVALUE }) + { values.add("MAXVALUE"); } + | expression=Expression() { values.add(expression.toString()); } + ) + )* { - return columnDefinition; + return values; } } -XmlTableFunction XmlTableBody() : { - XmlTableFunction function = new XmlTableFunction(); - Expression rowPath; - XmlTableFunction.XmlTablePassingClause passingClause; - XmlTableFunction.XmlTableColumnDefinition columnDefinition; +PartitionDefinition PartitionDefinition(): +{ + PartitionDefinition partitionDefinition = new PartitionDefinition(); + List values = null; + List subPartitionDefinitions = null; + String partitionName = null; } { - "(" - rowPath = Expression() { function.setRowPathExpression(rowPath); } - [ - LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) - - passingClause = XmlTablePassingClause() { function.addPassingClause(passingClause); } - ( - "," - passingClause = XmlTablePassingClause() { function.addPassingClause(passingClause); } - )* - ] + partitionName=RelObjectName() + { partitionDefinition.setPartitionName(partitionName); } [ - - columnDefinition = XmlTableColumnDefinition() { function.addColumnDefinition(columnDefinition); } + ( - "," - columnDefinition = XmlTableColumnDefinition() { function.addColumnDefinition(columnDefinition); } - )* + + { + partitionDefinition.setValueOperator( + PartitionDefinition.ValueOperator.LESS_THAN); + } + ( + + { + partitionDefinition.setMaxValue(true); + partitionDefinition.setValues(Collections.singletonList("MAXVALUE")); + } + | + "(" values=PartitionValues() ")" { partitionDefinition.setValues(values); } + ) + | + + { + partitionDefinition.setValueOperator(PartitionDefinition.ValueOperator.IN); + } + "(" values=PartitionValues() ")" { partitionDefinition.setValues(values); } + ) ] - ")" + ( PartitionDefinitionOption(partitionDefinition) )* + [ subPartitionDefinitions=SubpartitionDefinitions() + { partitionDefinition.setSubPartitionDefinitions(subPartitionDefinitions); } ] { - return function; + return partitionDefinition; } } -TableFunction TableFunction(): +List PartitionDefinitions(): { - Token prefix = null; - Function function = null; - ParenthesedExpressionList rowsFromFunctions = null; - Token withClause = null; + List partitionDefinitions = new ArrayList(); + PartitionDefinition partitionDefinition = null; } { - [ prefix = ] - ( - LOOKAHEAD(3) rowsFromFunctions = RowsFromFunctionList() - | - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("JSON_TABLE") - }) - JsonKeyword("JSON_TABLE") - function = JsonTableBody() - | - LOOKAHEAD({ - getToken(1).kind == S_IDENTIFIER - && getToken(1).image.equalsIgnoreCase("XMLTABLE") - }) - - function = XmlTableBody() - | - function=Function() - ) - [ LOOKAHEAD(2) ( withClause = | withClause = ) ] + "(" partitionDefinition=PartitionDefinition() + { partitionDefinitions.add(partitionDefinition); } + ( "," partitionDefinition=PartitionDefinition() + { partitionDefinitions.add(partitionDefinition); } )* + ")" { - if (rowsFromFunctions != null) { - return prefix != null - ? withClause != null - ? new TableFunction(prefix.image, rowsFromFunctions, withClause.image) - : new TableFunction(prefix.image, rowsFromFunctions) - : withClause != null - ? new TableFunction(rowsFromFunctions, withClause.image) - : new TableFunction(rowsFromFunctions); - } - - return prefix != null - ? withClause != null - ? new TableFunction(prefix.image, function, withClause.image) - : new TableFunction(prefix.image, function) - : withClause != null - ? new TableFunction(function, withClause.image) - : new TableFunction(function); + return partitionDefinitions; } } -ParenthesedExpressionList RowsFromFunctionList(): +PartitionDefinition SubpartitionDefinition(): { - ParenthesedExpressionList functions = new ParenthesedExpressionList(); - Function function; + PartitionDefinition partitionDefinition = new PartitionDefinition(); + String partitionName = null; } { - "(" - function = Function() { functions.add(function); } - ( - "," - function = Function() { functions.add(function); } - )* - ")" + partitionName=RelObjectName() { - return functions; + partitionDefinition.setPartitionName(partitionName); + partitionDefinition.setSubPartition(true); + } + ( PartitionDefinitionOption(partitionDefinition) )* + { + return partitionDefinition; } } -List ColumnNamesWithParamsList() : { - List colNames = new ArrayList(); - String columnName; - List parameter = null; +List SubpartitionDefinitions(): +{ + List partitionDefinitions = new ArrayList(); + PartitionDefinition partitionDefinition = null; } { - "(" - columnName=RelObjectName() - - { parameter = null; } - [ parameter = CreateParameter() ] + "(" partitionDefinition=SubpartitionDefinition() + { partitionDefinitions.add(partitionDefinition); } + ( "," partitionDefinition=SubpartitionDefinition() + { partitionDefinitions.add(partitionDefinition); } )* + ")" { - colNames.add(new Index.ColumnParams(columnName,parameter)); + return partitionDefinitions; } - - ( - "," - columnName=RelObjectName() - - { parameter = null; } - [ parameter = CreateParameter() ] - { - colNames.add(new Index.ColumnParams(columnName,parameter)); - } - )* - - ")" - - { return colNames; } } -Index.ColumnParams IndexColumnWithParams(): { - String columnName = null; - List parameter = null; - Expression expression = null; - Index.ColumnParams column = null; +void PartitionDefinitionOption(PartitionDefinition partitionDefinition): +{ + Token tk = null; + String value = null; + boolean useEquals = false; } { ( - columnName=RelObjectName() - { parameter = null; } - [ parameter = CreateParameter() ] + [ { partitionDefinition.setStorageKeyword(true); } ] + [ "=" { useEquals = true; } ] value=RelObjectName() { - column = new Index.ColumnParams(columnName, parameter); + partitionDefinition.setStorageEngine(value); + partitionDefinition.setStorageEngineUseEquals(useEquals); } | - "(" expression=Expression() ")" - { parameter = null; } - [ LOOKAHEAD(2) parameter = CreateParameter() ] + [ "=" { useEquals = true; } ] tk= { - column = new Index.ColumnParams(expression, parameter); + partitionDefinition.setCommentValue(stringValue(tk)); + partitionDefinition.setCommentUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] tk= + { + partitionDefinition.setDataDirectory(tk.image); + partitionDefinition.setDataDirectoryUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] tk= + { + partitionDefinition.setIndexDirectory(tk.image); + partitionDefinition.setIndexDirectoryUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] tk= + { + partitionDefinition.setMaxRows(tk.image); + partitionDefinition.setMaxRowsUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] tk= + { + partitionDefinition.setMinRows(tk.image); + partitionDefinition.setMinRowsUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] value=RelObjectName() + { + partitionDefinition.setTablespace(value); + partitionDefinition.setTablespaceUseEquals(useEquals); } ) - { - return column; - } } -List IndexColumnsWithParamsList() : { - List colNames = new ArrayList(); - Index.ColumnParams column = null; +List PartitionNamesList() : +{ + String partitionName = null; + List partitionNames = new ArrayList(); } { - "(" - column=IndexColumnWithParams() - { - colNames.add(column); + partitionName = RelObjectName() { + partitionNames.add(partitionName); } - ( - "," - column=IndexColumnWithParams() - { - colNames.add(column); + LOOKAHEAD(2) "," partitionName = RelObjectName() { + partitionNames.add(partitionName); } )* + { + return partitionNames; + } +} - ")" +/** + * Parses DISCARD/IMPORT (PARTITION names TABLESPACE | TABLESPACE). + * Both keywords share the same structure, differing only in operation enum. + */ +AlterExpression AlterExpressionDiscardOrImport(): +{ + AlterExpression alterExp; + List partitions = null; + AlterOperation partOp; + AlterOperation tableOp; +} +{ + ( + { partOp = AlterOperation.DISCARD_PARTITION; tableOp = AlterOperation.DISCARD_TABLESPACE; } + | + { partOp = AlterOperation.IMPORT_PARTITION; tableOp = AlterOperation.IMPORT_TABLESPACE; } + ) + ( + + { alterExp = new AlterExpressionPartition(); alterExp.setOperation(partOp); } + partitions = PartitionNamesList() + { alterExp.setPartitions(partitions); } + + { alterExp.setTableOption("TABLESPACE"); } + | + + { alterExp = new AlterExpressionTableOption(); alterExp.setOperation(tableOp); } + ) + { return alterExp; } +} - { return colNames; } + +/** + * Parses SQL Server's [CONSTRAINT name] DEFAULT expression FOR column [WITH VALUES]. + */ +DefaultConstraint DefaultConstraintSpec(): +{ + DefaultConstraint constraint = new DefaultConstraint(); + String name; + Expression expression; +} +{ + [ name=RelObjectName() { constraint.setName(name); } ] + expression=Expression() { constraint.setExpression(expression); } + name=RelObjectName() { constraint.setColumn(linkToLastToken(createColumn(name))); } + // See InsertBulk(): is in the FOLLOW set of every statement tail. WITH VALUES wins + // over a hypothetical CTE named VALUES, which is the useful reading here. + [ LOOKAHEAD(2) { constraint.setWithValues(true); } ] + { return constraint; } +} + +/** Parses Informix's ADD CONSTRAINT definition [CONSTRAINT name]. */ +NamedConstraint InformixConstraint(): +{ + NamedConstraint constraint; + Token type; + List columns; + String name; +} +{ + + ( + ( type= | type= ) + columns=ColumnsNamesList() { + constraint = new KeyConstraint() + .withType(type.kind == K_PRIMARY ? "PRIMARY KEY" : type.image) + .withColumnsNames(columns); + } + | + constraint=ForeignKeySpec(null) + | + constraint=CheckConstraintSpec(null) + ) + [ name=RelObjectName() { constraint.setName(name); } ] + { + constraint.setUseConstraintKeyword(true); + constraint.setConstraintNamePosition(NamedConstraint.ConstraintNamePosition.AFTER); + return constraint; + } } -Index Index(): { - ObjectNames name; +/** + * Parses ADD/ALTER CONSTRAINT clause within AlterExpression. + * Handles: CONSTRAINT [UNIQUE [KEY|INDEX]] name columns + * CONSTRAINT name (FOREIGN KEY|PRIMARY KEY|UNIQUE|KEY|CHECK|[NOT] ENFORCED) + */ +void AlterExpressionAddConstraint(AlterExpression alterExp): +{ + Token tk; + Token tk2 = null; + String sk3 = null; + List columnNames = null; + ForeignKeyConstraint fkIndex = null; + KeyConstraint index = null; + Table fkTable; + List constraints = null; + CheckConstraint checkCs = null; + IndexOptions.Clustering clustering = null; + String using = null; } { - name= RelObjectNames() { return new Index().withName(name.getNames()).withType(""); } + + ( + LOOKAHEAD(2) + ( + ( { alterExp.setConstraintType("UNIQUE KEY"); } + | { alterExp.setConstraintType("UNIQUE INDEX"); } + | { alterExp.setConstraintType("UNIQUE"); } ) + sk3=RelObjectName() { + alterExp.setConstraintSymbol(sk3); + index = new KeyConstraint().withType(alterExp.getConstraintType()) + .withIndexName(sk3).withUseConstraintKeyword(true); + } + columnNames=ColumnsNamesList() { + index.setColumnsNames(columnNames); + alterExp.setConstraint(index); + } + ) + | + sk3=RelObjectName() + ( + ( fkIndex=ForeignKeySpec(sk3) + { alterExp.setConstraint(fkIndex); } + constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } + ) + | + ( tk= tk2= + clustering=SqlServerIndexClustering() + [ LOOKAHEAD({ getToken(1).kind == K_USING && getToken(2).kind != K_INDEX }) + using=UsingIndexType() ] + columnNames=ColumnsNamesList() + { + index = new KeyConstraint() + .withName(sk3) + .withType(tk.image + " " + tk2.image) + .withClustering(clustering).withUsing(using) + .withColumnsNames(columnNames); + alterExp.setConstraint(index); + } + AlterExpressionConstraintTail(alterExp, index) + ) + | + LOOKAHEAD(2) ( + { boolean enforced = true; } + [ tk = { enforced = false; } ] + { + alterExp.setEnforced(enforced); + alterExp.setConstraintType("CONSTRAINT"); + alterExp.setConstraintSymbol(sk3); + } + ) + | + ( + checkCs = CheckConstraintSpec(sk3) + { alterExp.setConstraint(checkCs); } + ) + | + ( + tk= (tk2= | tk2=)? + clustering=SqlServerIndexClustering() + [ LOOKAHEAD({ getToken(1).kind == K_USING && getToken(2).kind != K_INDEX }) + using=UsingIndexType() ] + columnNames=ColumnsNamesList() + { + index = new KeyConstraint() + .withName(sk3) + .withType(tk.image + (tk2!=null?" " + tk2.image:"")) + .withClustering(clustering).withUsing(using) + .withColumnsNames(columnNames); + alterExp.setConstraint(index); + } + AlterExpressionConstraintTail(alterExp, index) + ) + | + ( + tk= + columnNames=ColumnsNamesList() + { + index = new KeyConstraint() + .withName(sk3) + .withType(tk.image) + .withColumnsNames(columnNames); + alterExp.setConstraint(index); + } + constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } + ) + ) + ) } -CreateIndex CreateIndex(): +/** + * Parses the DROP operations within AlterExpression. + * Handles: DROP (PARTITION|columns|COLUMN|INDEX|KEY|UNIQUE|PRIMARY KEY|FOREIGN KEY|CONSTRAINT) + */ +AlterExpression AlterExpressionDrop(): { - CreateIndex createIndex = new CreateIndex(); - Table table = null; - List colNames; - String indexType; + AlterExpressionDrop drop = new AlterExpressionDrop(); + AlterExpression alterExp = drop; + CascadeBehavior behavior; + Token tk; + Token tk2; + List columnNames = null; + List partitions = null; Index index = null; - List parameter = new ArrayList(); - List tailParameters = new ArrayList(); - List name; } { - [ parameter=CreateParameter() ] - - - [ LOOKAHEAD(2) { createIndex.setUsingIfNotExists(true);} ] - index = Index() { index.setType(parameter.isEmpty() ? null : parameter.get(0)); } + { alterExp.setOperation(AlterOperation.DROP); } ( - LOOKAHEAD(3)( - table=Table() - [ indexType=UsingIndexType() { index.setUsing(indexType); } ] + LOOKAHEAD({ isNamedConstraintDropAhead() }) + MySqlDropNamedConstraint(alterExp) + | + ( + { + alterExp = new AlterExpressionPartition(); + alterExp.setOperation(AlterOperation.DROP_PARTITION); + } + partitions=PartitionNamesList() { + alterExp.setPartitions(partitions); + } ) | ( - [ indexType=UsingIndexType() { - index.setUsing(indexType); - createIndex.setIndexTypeBeforeOn(true); - } + // Oracle Multi Column Drop + columnNames=ColumnsNamesList() { alterExp.setDropColumns(columnNames); columnNames = null; } + [ "INVALIDATE" { alterExp.addParameters("INVALIDATE"); } ] + [ + "CASCADE" { alterExp.addParameters("CASCADE"); } + [ "CONSTRAINTS" { alterExp.addParameters("CONSTRAINTS"); } ] + ] + ) + | + ( + ( LOOKAHEAD(2) { alterExp.hasColumn(true); } )? + [ { alterExp.setUsingIfExists(true); } ] + (tk=KeywordOrIdentifier() ) { alterExp.setColumnName(tk.image); } + [ "INVALIDATE" { alterExp.addParameters("INVALIDATE"); } ] + [ + // Oracle CASCADE CONSTRAINTS remains a raw parameter + LOOKAHEAD(2) "CASCADE" "CONSTRAINTS" { alterExp.addParameters("CASCADE", "CONSTRAINTS"); } + | behavior=TypeAlterBehavior() { drop.setBehavior(behavior); } ] - table=Table() + ) + | + ( + ( tk= | tk= ) + ( tk2= | tk2= ) { + index = new Index().withType(tk.image).withName(tk2.image); + alterExp.setIndex(index); + } + ) + | + ( + tk= { alterExp.setOperation(AlterOperation.DROP_UNIQUE); } + columnNames=ColumnsNamesList() { alterExp.setDropColumns(columnNames); columnNames = null; } + [ behavior=TypeAlterBehavior() { drop.setBehavior(behavior); } ] + ) + | + ( + tk= tk2= { alterExp.setOperation(AlterOperation.DROP_PRIMARY_KEY); } + [ behavior=TypeAlterBehavior() { drop.setBehavior(behavior); } ] + ) + | + ( + tk= tk2= { alterExp.setOperation(AlterOperation.DROP_FOREIGN_KEY); } + columnNames=ColumnsNamesList() { alterExp.setDropColumns(columnNames); columnNames = null; } + [ behavior=TypeAlterBehavior() { drop.setBehavior(behavior); } ] + ) + | + ( + [ { alterExp.setUsingIfExists(true); } ] + ( tk= | tk=) { alterExp.setConstraintName(tk.image); } + [ behavior=TypeAlterBehavior() { drop.setBehavior(behavior); } ] ) ) - colNames = IndexColumnsWithParamsList() - ( LOOKAHEAD(2) parameter=CreateParameter() { tailParameters.addAll(parameter); } )* - { - index.setColumns(colNames); - createIndex.setIndex(index); - createIndex.setTable(table); - createIndex.setTailParameters(tailParameters); - return createIndex; - } + { return alterExp; } } -ColumnDefinition ColumnDefinition(): { - ColumnDefinition coldef; - String columnName; - ColDataType colDataType; - List columnSpecs = new ArrayList(); - List parameter; -} { - columnName=RelObjectName() - colDataType=ColDataType() - ( LOOKAHEAD(2) parameter=CreateParameter() { columnSpecs.addAll(parameter); } )* - { - coldef = new ColumnDefinition(); - coldef.setColumnName(columnName); - coldef.setColDataType(colDataType); - if (columnSpecs.size() > 0) - coldef.setColumnSpecs(columnSpecs); - return coldef; - } +/** A MySQL deletion target is a constraint name, not a list of key columns. */ +void MySqlDropNamedConstraint(AlterExpression alterExp): +{ String name; } +{ + ( { alterExp.setOperation(AlterOperation.DROP_FOREIGN_KEY); } + | { alterExp.setOperation(AlterOperation.DROP_CHECK); } ) + name=RelObjectName() { alterExp.setConstraintName(name); } } -CreateSchema CreateSchema(): +/** + * Parses partition maintenance operations within AlterExpression. + * Handles: TRUNCATE/ANALYZE/CHECK/OPTIMIZE/REBUILD/REPAIR PARTITION, + * COALESCE/REORGANIZE/EXCHANGE/PARTITION BY, REMOVE PARTITIONING + */ +AlterExpression AlterExpressionPartitionOp(): { - Token tk = null; - CreateTable table = null; - CreateView view = null; - CreateSchema schema = new CreateSchema(); - List schemaPath = null; - List statements = new ArrayList(); + AlterExpressionPartition alterExp = new AlterExpressionPartition(); + AlterOperation operation; + Token tk; + List partitions = null; + List partitionDefinition = null; + Table exchangeTable = null; + TablePartitioning partitioning = null; } { - - [ LOOKAHEAD(2) { schema.setIfNotExists(true); } ] - [ - ( tk= | tk=) { schema.setSchemaName(tk.image); } - - ( - "." { schema.setCatalogName(tk.image); } - ( tk= | tk=) { schema.setSchemaName(tk.image); } - )? - ] - - [ - (tk= | tk=) { schema.setAuthorization(tk.image); } - ] - - [schemaPath=PathSpecification() { schema.setSchemaPath(schemaPath); }] - ( LOOKAHEAD(2) ( - - - table = CreateTable(false) - { - table.getTable().setSchemaName(schema.getSchemaName()); - schema.addStatement(table); - } - | - view = CreateView(false) - { - view.getView().setSchemaName(schema.getSchemaName()); - schema.addStatement(view); - } + { operation = AlterOperation.TRUNCATE_PARTITION; } + | { operation = AlterOperation.ANALYZE_PARTITION; } + | { operation = AlterOperation.CHECK_PARTITION; } + | { operation = AlterOperation.OPTIMIZE_PARTITION; } + | { operation = AlterOperation.REBUILD_PARTITION; } + | { operation = AlterOperation.REPAIR_PARTITION; } ) - )* - { - return schema; - } + { alterExp.setOperation(operation); } + [ + LOOKAHEAD({ operation != AlterOperation.TRUNCATE_PARTITION + && operation != AlterOperation.CHECK_PARTITION + && isPartitionBinlogModifierAhead() }) + PartitionBinlogModifier(alterExp) + ] + partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } + | + LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.COALESCE_PARTITION); } + [ PartitionBinlogModifier(alterExp) ] + tk= { alterExp.setPartitionCount(Integer.valueOf(tk.image)); } + | + LOOKAHEAD(2) + [ LOOKAHEAD({ isPartitionBinlogModifierAhead() }) PartitionBinlogModifier(alterExp) ] + partitions=PartitionNamesList() partitionDefinition=PartitionDefinitions() { + alterExp.setOperation(AlterOperation.REORGANIZE_PARTITION); + alterExp.setPartitions(partitions); + alterExp.setPartitionDefinitions(partitionDefinition); + } + | + LOOKAHEAD(2) partitions=PartitionNamesList() + exchangeTable=Table() + [ + LOOKAHEAD(2) ( + { + alterExp.setExchangeValidationMode(AlterExpressionPartition.ExchangeValidationMode.WITH); + } + | + { + alterExp.setExchangeValidationMode(AlterExpressionPartition.ExchangeValidationMode.WITHOUT); + } + ) + ] + { + alterExp.setOperation(AlterOperation.EXCHANGE_PARTITION); + alterExp.setPartitions(partitions); + alterExp.setExchangeTable(exchangeTable); + } + | + LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.REMOVE_PARTITIONING); } + | + LOOKAHEAD(2) partitioning=CreateTablePartitioning() { + alterExp.setOperation(AlterOperation.PARTITION_BY); + alterExp.setPartitioning(partitioning); + } + ) + { return alterExp; } } -List PathSpecification(): +/** + * Parses MySQL ADD PARTITION [NO_WRITE_TO_BINLOG | LOCAL] {(definitions) | PARTITIONS n} + * after ADD. + */ +AlterExpressionPartition AddPartition(): { + AlterExpressionPartition alterExp = new AlterExpressionPartition(); + List partitionDefinitions; Token tk; - List pathList = new ArrayList(); } { - (tk=|tk=) { pathList.add(tk.image); } - ("," (tk=|tk=) { pathList.add(tk.image); })* + { alterExp.setOperation(AlterOperation.ADD_PARTITION); } + [ PartitionBinlogModifier(alterExp) ] + ( + partitionDefinitions=PartitionDefinitions() { + alterExp.setPartitionDefinitions(partitionDefinitions); + } + | + tk= { alterExp.setPartitionCount(Integer.valueOf(tk.image)); } + ) + { return alterExp; } +} + +/** Parses MySQL's opt_no_write_to_binlog of a partition operation. */ +void PartitionBinlogModifier(AlterExpressionPartition alterExp): +{ + Token tk; +} +{ + ( tk= | tk= ) { - return pathList; + alterExp.setModifier(TableMaintenanceStatement.Modifier.valueOf( + tk.image.toUpperCase(Locale.ROOT))); } } /** - * Parses a single table-level constraint inside CREATE TABLE (...). - * Handles INDEX, PRIMARY KEY, UNIQUE, KEY, FOREIGN KEY, CHECK, EXCLUDE. - * Returns an Index (which may be NamedConstraint, ForeignKeyIndex, CheckConstraint, ExcludeConstraint). + * Parses ADD/ALTER/MODIFY operations within ALTER TABLE. + * Handles: PRIMARY KEY, INDEX/KEY, SPATIAL/FULLTEXT, column COMMENT, + * ADD PARTITION, column definitions, UNIQUE, FOREIGN KEY, CHECK ENFORCED, CONSTRAINT. */ -Index CreateTableConstraint(): +AlterExpression AlterExpressionAddAlterModify(): { - Token tk = null; + AlterExpression alterExp = new AlterExpression(); + Token tk; Token tk2 = null; - Token tk3 = null; String sk3 = null; - List colNames = null; - List parameter = new ArrayList(); - List idxSpec = new ArrayList(); + String sk4 = null; + List columnNames = null; + List indexColumnNames = null; + List constraints = null; Index index = null; - ForeignKeyIndex fkIndex = null; - CheckConstraint checkCs = null; - ExcludeConstraint excludeC = null; - Expression exp = null; + NamedConstraint constraint; + TableElement element; + AlterExpression.ColumnDataType alterExpressionColumnDataType = null; + AlterExpression.ColumnDropNotNull alterExpressionColumnDropNotNull = null; + List indexSpec = new ArrayList(); } { ( - LOOKAHEAD(4) ( - { idxSpec.clear(); tk3=null; } - [ tk3= ] - tk= - sk3=RelObjectName() - colNames = IndexColumnsWithParamsList() - ( parameter=CreateParameter() { idxSpec.addAll(parameter); } )* - { - index = new Index().withType((tk3!=null ? tk3.image + " " : "") + tk.image).withName(sk3).withColumns(colNames).withIndexSpec(new ArrayList(idxSpec)); - } + { alterExp.setOperation(AlterOperation.ADD); } + | + { alterExp.setOperation(AlterOperation.ALTER); } + | + { alterExp.setOperation(AlterOperation.MODIFY); } + ) + + ( + LOOKAHEAD({ getToken(1).kind == K_DEFAULT || getToken(1).kind == K_CONSTRAINT + && getToken(3).kind == K_DEFAULT }) constraint=DefaultConstraintSpec() { + requireDdlSyntax(alterExp.getOperation() == AlterOperation.ADD, + "Table-level DEFAULT constraints require ADD"); + alterExp.setConstraint(constraint); + } + | + LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ALTER + && getToken(1).kind == K_INDEX }) + tk= sk3=RelObjectName() TableIndexOptions(false, indexSpec) { + index = new Index().withIndexKeyword(tk.image).withName(sk3) + .withKind(Index.Kind.INDEX).withSpecOptions(indexSpec); + alterExp.setIndex(index); + } + | + LOOKAHEAD( ( | + | | ) "(", + { Dialect.INFORMIX.name().equals(getAsString(Feature.dialect)) }) + constraint=InformixConstraint() { + requireDdlSyntax(alterExp.getOperation() == AlterOperation.ADD, + "Informix constraint definitions require ADD"); + alterExp.setConstraint(constraint); + } + | + LOOKAHEAD([ RelObjectName() ] + ( | ) , + { alterExp.getOperation() == AlterOperation.ADD + && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) }) + constraint=PostgreSqlConstraintUsingIndex() { alterExp.setConstraint(constraint); } + | + LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ADD + && ((Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + || Dialect.MYSQL.name().equals(getAsString(Feature.dialect))) + && (getToken(1).kind == K_CONSTRAINT || getToken(1).kind == K_PRIMARY + || getToken(1).kind == K_UNIQUE || getToken(1).kind == K_FOREIGN + || getToken(1).kind == K_CHECK || getToken(1).kind == K_EXCLUDE + || getToken(1).kind == K_NOT + && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) + || AlterConstraintLookahead.needsSharedTableConstraint(getToken(1), token_source)) }) + element=TableConstraint(false) { setAlterTableElement(alterExp, element); } + | + LOOKAHEAD({ isTableIndexAhead() }) element=TableIndexSpec(false) { + setAlterTableElement(alterExp, element); + } + constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } + [ AlterExpressionUsingIndex(alterExp) ] + | + // Include the literal: COLUMN comment is a column definition, not a COMMENT clause. + LOOKAHEAD(RelObjectName() ) ( + sk3=RelObjectName() tk= { alterExp.withColumnName(sk3).withCommentText(tk.image); } ) | + LOOKAHEAD( [ | ] + ( "(" | ), + { alterExp.getOperation() == AlterOperation.ADD }) + alterExp=AddPartition() + | LOOKAHEAD(3) ( - { - index = new NamedConstraint(); - tk2=null; - idxSpec.clear(); - } - [ sk3=RelObjectName() {index.setName(sk3);} ] + ( LOOKAHEAD(2) + ( + { alterExp.hasColumn(true); } + | + { alterExp.hasColumns(true); } + ) + )? + [ + LOOKAHEAD(2) + ( + { alterExp.setUseIfNotExists(true); } + | + { alterExp.setUsingIfExists(true); } + ) + ] ( - tk= tk2= + LOOKAHEAD(3) AlterExpressionColumnChanges(alterExp) + | + LOOKAHEAD(3) alterExpressionColumnDropNotNull = AlterExpressionColumnDropNotNull() + { alterExp.addColDropNotNull( alterExpressionColumnDropNotNull);} | - tk= [ tk2= ] + LOOKAHEAD(2) alterExpressionColumnDataType = AlterExpressionColumnDataType() + { alterExp.addColDataType(alterExpressionColumnDataType); } + // T-SQL and Snowflake let one ADD list several columns and table + // constraints without repeating ADD. + ( + LOOKAHEAD(",", { alterExp.getOperation() == AlterOperation.ADD + && (Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + || Dialect.SNOWFLAKE.name().equals(getAsString(Feature.dialect))) + && isAlterAddListItemAhead() }) + "," { alterExp.setBareAddList(true); } + ( + LOOKAHEAD({ getToken(1).kind == K_CONSTRAINT || getToken(1).kind == K_CHECK + || getToken(1).kind == K_PRIMARY || getToken(1).kind == K_UNIQUE + || getToken(1).kind == K_FOREIGN }) + element=TableConstraint(false) { setAlterTableElement(alterExp, element); } + | + alterExpressionColumnDataType = AlterExpressionColumnDataType() + { alterExp.addColDataType(alterExpressionColumnDataType); } + ) + )* ) - { - index.setType( tk.image + ( tk2!=null ? " " + tk2.image : "" )); - tk2=null; - } - colNames = ColumnNamesWithParamsList() - ( parameter=CreateParameter() { idxSpec.addAll(parameter); } )* - { - index.withColumns(colNames).withIndexSpec(new ArrayList(idxSpec)); - } ) | - LOOKAHEAD(3) ( - { - tk=null; - tk3=null; - idxSpec.clear(); - } - [ tk= ] - [ tk3= | tk3= ] tk2= - sk3=RelObjectName() - colNames = IndexColumnsWithParamsList() - ( parameter=CreateParameter() { idxSpec.addAll(parameter); } )* + LOOKAHEAD(3) AlterExpressionColumnChanges(alterExp) + | + // Standalone FK now uses ForeignKeyConstraint, same as CONSTRAINT FK + ( + { ForeignKeyConstraint fkIndex; } + fkIndex = ForeignKeySpec(null) { - index = new Index() - .withType( ( tk!=null ? tk.image + " " : "") + ( tk3!=null ? tk3.image + " ":"" ) + tk2.image) - .withName(sk3) - .withColumns(colNames) - .withIndexSpec(new ArrayList(idxSpec)); + setAlterTableElement(alterExp, fkIndex); } ) | LOOKAHEAD(3) ( - { sk3=null; } - [ sk3=RelObjectName() ] - fkIndex = ForeignKeySpec(sk3) - { index = fkIndex; } - ) - | - LOOKAHEAD(3) ( - { sk3 = null; } - [ sk3 = RelObjectName() ] - checkCs = CheckConstraintSpec(sk3) - { index = checkCs; } + sk3=RelObjectName() + { boolean enforced = true; } + [ tk = { enforced = false; } ] + { + alterExp.setEnforced(enforced); + alterExp.setConstraintType("CHECK"); + alterExp.setConstraintSymbol(sk3); + } ) | - LOOKAHEAD(2) ( - tk= { excludeC = new ExcludeConstraint(); } - (tk2= - ("(" exp = Expression() ")")* {excludeC.setExpression(exp);}) - { index = excludeC; } + ( + AlterExpressionAddConstraint(alterExp) ) ) - { - return index; - } + { return alterExp; } } - -CreateTable CreateTable(boolean isUsingOrReplace): +/** Parses CockroachDB primary-key elements without legacy raw index parameters. */ +KeyElement CockroachPrimaryKeyColumn(): { - CreateTable createTable = new CreateTable(); - Table table = null; - List columnDefinitions = new ArrayList(); - List tableOptions = new ArrayList(); - List createOptions = new ArrayList(); - Token tk = null; - ColumnDefinition coldef = null; - List indexes = new ArrayList(); - Index index = null; - List parameter = new ArrayList(); - SpannerInterleaveIn interleaveIn = null; - Select select = null; - Table likeTable = null; - RowMovement rowMovement = null; - String tableColumn = null; - List columns = new ArrayList(); + KeyElement column; + String name; + Expression expression; + Token token; } { - { createTable.setOrReplace(isUsingOrReplace);} - [ { createTable.setUnlogged(true); } ] - - // table options, not required but 1 or none - [ tk= { createOptions.add(tk.image);} ] - - (parameter = CreateParameter() { createOptions.addAll(parameter); })* - - - [ LOOKAHEAD(2) { createTable.setIfNotExists(true); }] - table=Table() - [ LOOKAHEAD(2) ( - LOOKAHEAD(3) ( - "(" tableColumn=RelObjectName() { columns.add(tableColumn); } ("," tableColumn=RelObjectName() { columns.add(tableColumn); } )* ")" - ) + ( + LOOKAHEAD({ isFunctionAhead() }) expression=Function() + { column = new KeyElement(expression).withExpressionParenthesized(false); } | - ( - "(" - coldef = ColumnDefinition() { columnDefinitions.add(coldef); } - - ( - "," - ( - LOOKAHEAD(3) ( - index = CreateTableConstraint() - { indexes.add(index); } - ) - | - ( - coldef = ColumnDefinition() - { columnDefinitions.add(coldef); } - ) - ) - )* - - ")" - ) - ) - ] - ( LOOKAHEAD(2, { getToken(1).kind != K_AS }) parameter=CreateParameter() { tableOptions.addAll(parameter); } )* - - // see https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_7002.htm#i2126725 - // table properties , all these are optional - [ rowMovement = RowMovement() { createTable.setRowMovement(rowMovement); }] - [ select = Select() { createTable.setSelect(select, false); }] - [ - ( LOOKAHEAD("(" Table() ")") "(" likeTable=Table() { createTable.setLikeTable(likeTable, true); } ")" - | likeTable=Table() { createTable.setLikeTable(likeTable, false); } ) - ] - [ interleaveIn = SpannerInterleaveIn( ) { createTable.setSpannerInterleaveIn(interleaveIn); } ] - { - createTable.setTable(table); - if (indexes.size() > 0) - createTable.setIndexes(indexes); - if (createOptions.size() > 0) - createTable.setCreateOptionsStrings(createOptions); - if (tableOptions.size() > 0) - createTable.setTableOptionsStrings(tableOptions); - if (columnDefinitions.size() > 0) - createTable.setColumnDefinitions(columnDefinitions); - if (columns.size() > 0) - createTable.setColumns(columns); - return createTable; - } + name=RelObjectName() { column = new KeyElement(name); } + | + "(" expression=Expression() ")" { column = new KeyElement(expression); } + ) + [ (token= | token=) { column.setOperatorClass(token.image); } ] + [ (token= | token=) { + column.setSortOrder(KeyElement.SortOrder.valueOf(token.image.toUpperCase(Locale.ROOT))); + } ] + [ (token= | token=) { + column.setNullOrdering(KeyElement.NullOrdering.valueOf(token.image.toUpperCase(Locale.ROOT))); + } ] + { return column; } } -SpannerInterleaveIn SpannerInterleaveIn(): +AlterExpressionPrimaryKey CockroachAlterPrimaryKey(): { - Table table = null; - SpannerInterleaveIn.OnDelete action = null; + AlterExpressionPrimaryKey action = new AlterExpressionPrimaryKey(); + List columns = new ArrayList(); + KeyElement column; + List options; + Expression bucketCount; + Token token; } { - table = Table() - [ - - ( - { action = SpannerInterleaveIn.OnDelete.NO_ACTION; } - | { action = SpannerInterleaveIn.OnDelete.CASCADE; } - ) + + "(" column=CockroachPrimaryKeyColumn() { columns.add(column); } + ( "," column=CockroachPrimaryKeyColumn() { columns.add(column); } )* ")" + { action.getConstraint().setColumns(columns); } + [ { action.setUsingHash(true); } + [ LOOKAHEAD( ) token= { + requireDdlSyntax("BUCKET_COUNT".equalsIgnoreCase(token.image), "Expected BUCKET_COUNT"); + } + "=" bucketCount=Expression() { action.setBucketCount(bucketCount); } ] ] - { - return new SpannerInterleaveIn(table, action); - } + // LOOKAHEAD(2) as in the identical constructs at CreateIndex() and AlterExpression(). + [ LOOKAHEAD(2) options=PostgreSqlIndexOptions() { action.getConstraint().setStorageParameters(options); } ] + { return action; } } - -ColDataType DataType(): +/** + * Parses all RENAME variants within ALTER TABLE. + * Handles: RENAME [COLUMN] old TO new, RENAME [TO|AS] tablename, RENAME tablename, + * RENAME INDEX/KEY/CONSTRAINT old TO new. + */ +AlterExpression AlterExpressionRenameOp(): { - ColDataType colDataType = new ColDataType(); - Token prefix = null; - Token tk = null; - Token tk2 = null; - String schema; - String type=""; - List argumentsStringList = new ArrayList(); - List array = new ArrayList(); - List name; - ColDataType arrayType; - - int precision = -1; - int scale = -1; + AlterExpression alterExp = new AlterExpressionRename(); + Token tk; + Token tk2; + Index index; + Table newTable; } { + ( - LOOKAHEAD(2) tk= { - type = tk.image; - return new ColDataType(type, precision, scale); + LOOKAHEAD(2) ( + ( + { alterExp.setOperation(AlterOperation.RENAME_INDEX); } + | { alterExp.setOperation(AlterOperation.RENAME_KEY); } + ) + | + { alterExp.setOperation(AlterOperation.RENAME_CONSTRAINT); } + ) + (tk= | tk=) { + if (alterExp.getOperation() == AlterOperation.RENAME_CONSTRAINT) { + alterExp.setConstraintName(tk.image); + } else { + alterExp.setOldIndex(new Index().withName(tk.image)); + } + } + + (tk2= | tk2=) { + if (alterExp.getOperation() == AlterOperation.RENAME_CONSTRAINT) { + alterExp.setNewConstraintName(tk2.image); + } else { + index = new Index().withName(tk2.image); + alterExp.setIndex(index); + } } | - LOOKAHEAD(2) tk= ( - ("<" arrayType = ColDataType() ">") { - colDataType.setDataType("ARRAY<" + arrayType.getDataType() + ">"); + LOOKAHEAD({ getToken(1).kind == K_TO || getToken(1).kind == K_AS }) ( + (tk= | tk=) { + alterExp.setOperation(AlterOperation.RENAME_TABLE); + alterExp.setTableRenameKeyword(AlterExpression.TableRenameKeyword.valueOf( + tk.image.toUpperCase(Locale.ROOT))); } + newTable=Table() { alterExp.setNewTable(newTable); } + ) + | + LOOKAHEAD(3) + ( + { alterExp.setOperation(AlterOperation.RENAME); } + [ { alterExp.hasColumn(true); } ] + (tk=KeywordOrIdentifier()) { alterExp.setColOldName(tk.image); } + + (tk2=KeywordOrIdentifier()) { alterExp.setColumnName(tk2.image); } ) | ( - ( tk= | tk= | tk = | tk = | tk = - | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= ) { type = tk.image; } - [ - // MySQL seems to allow: INT UNSIGNED - LOOKAHEAD(2) ( LOOKAHEAD(2) ( tk = | tk = | tk = - | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= ) { type += " " + tk.image; } )+ - ] - [ - LOOKAHEAD(2) "(" ( tk= { precision = Integer.valueOf(tk.image); } | tk= { precision = Integer.MAX_VALUE; } ) - [ | ] - [ "," tk = { scale = Integer.valueOf(tk.image); } ] - ")" - ] { - colDataType = new ColDataType(type, precision, scale); + alterExp.setOperation(AlterOperation.RENAME_TABLE); + alterExp.setTableRenameKeyword(AlterExpression.TableRenameKeyword.NONE); } + newTable=Table() { alterExp.setNewTable(newTable); } ) ) - - { - return colDataType; - } + { return alterExp; } } -ColDataType ColDataType(): +AlterExpressionPartition AlterExpressionPostgreSqlPartitionOp(): { - ColDataType colDataType = new ColDataType(); - Token prefix = null; - Token tk = null; - Token tk2 = null; - String schema; - String type=""; - List argumentsStringList = new ArrayList(); - List array = new ArrayList(); - List name; - ColDataType arrayType; - ColDataType nestedType = null; - IntervalQualifier intervalQualifier = null; - - int precision = -1; - int scale = -1; + AlterExpressionPartition alterExp = new AlterExpressionPartition(); + Table partitionTable = null; + PartitionBound partitionBound = null; } { ( - ( - - "(" - type = RelObjectNameExt() - colDataType = ColDataType() { argumentsStringList.add( type + " " + colDataType.toString()); } - ( - "," - type = RelObjectNameExt() - colDataType = ColDataType() { argumentsStringList.add( type + " " + colDataType.toString()); } - )* - ")" { colDataType = new ColDataType("STRUCT"); } - ) - | - LOOKAHEAD(2) ( - colDataType = DataType() - ) + partitionTable=Table() + partitionBound=PostgreSqlPartitionBound() + { + alterExp.setOperation(AlterOperation.ATTACH_PARTITION); + alterExp.setPartitionTable(partitionTable); + alterExp.setPartitionBound(partitionBound); + } | - ( - tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - | tk= - ) { schema = tk.image; } - - // Consume an optional INTERVAL qualifier such as `hour to minute` or - // `day(9) to second`. Only applicable when the matched type is an INTERVAL and - // is immediately followed by a field word, so other types (and the existing - // `interval (2)` precision form handled below) are unaffected. + partitionTable=Table() + { + alterExp.setOperation(AlterOperation.DETACH_PARTITION); + alterExp.setPartitionTable(partitionTable); + } [ - LOOKAHEAD({ "interval".equalsIgnoreCase(schema) - && getToken(1).kind == K_DATE_LITERAL }) - intervalQualifier = IntervalQualifier() { colDataType.setIntervalQualifier(intervalQualifier); } - ] - [ LOOKAHEAD(2) "." arrayType = ColDataType() { schema += "." + arrayType.toString(); } ] - { colDataType.setDataType(schema); } - ) - - [ - LOOKAHEAD(2) "(" {tk2 =null;} - ( - ( - LOOKAHEAD(( | ) "(") nestedType=ColDataType() - | - ( - ( - ( tk= | tk= ) [ LOOKAHEAD(2) (tk2= | tk2=) ] - ) - | - tk= - | - tk= - | - tk= - ) - ) - { - if (nestedType != null) { - argumentsStringList.add(nestedType.toString()); - nestedType = null; - } else { - argumentsStringList.add(tk.image + (tk2!=null?" " + tk2.image:"")); + { + alterExp.setDetachMode(AlterExpressionPartition.DetachMode.CONCURRENTLY); } + | + { + alterExp.setDetachMode(AlterExpressionPartition.DetachMode.FINALIZE); } - - [ "," ] - )* - ")" - ] - [ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk= ] { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ { colDataType.setArrayData(array); } ] - [ LOOKAHEAD(2) (tk= | tk=) { colDataType.setCharacterSet(tk.image); } ] - - { - if (argumentsStringList.size() > 0) - colDataType.setArgumentsStringList(argumentsStringList); - return colDataType; - } + ] + ) + { return alterExp; } } -Analyze Analyze(): +/** +* Dispatcher production for all ALTER TABLE expression types. +* Delegates to focused sub-productions for each operation category. +*/ +AlterConstraintAttributes AlterConstraintAttributes(): { - Analyze analyze = new Analyze(); - Table table = null; + AlterConstraintAttributes action = new AlterConstraintAttributes(); + String name; + ConstraintAttributes attributes; } { - - table=Table() - + name=RelObjectName() + attributes=PostgreSqlConstraintAttributeList(null) { - analyze.setTable(table); - return analyze; + requireDdlSyntax(attributes != null && !attributes.isNotValid(), + "ALTER CONSTRAINT requires attributes and does not accept NOT VALID"); + action.setConstraintSymbol(name); + action.setAttributes(attributes); + return action; } } -ExpressionList ColumnWithCommentList(): -{ - ExpressionList expressions = new ExpressionList(); - Column img = null; -} +AlterExpression AlterExpression(): { - "(" - img=Column() { expressions.add(img); } - ( "," img=Column() { expressions.add(img); } )* - ")" - { - return expressions; - } -} - + AlterExpression alterExp = null; + Token tk; + Token tk2 = null; + String sk3 = null; -CreateView CreateView(boolean isUsingOrReplace): -{ - CreateView createView = new CreateView(); - Table view = null; - Select select = null; - ExpressionList columnNames = null; - Token tk = null; - List commentTokens = null; + // for captureRest() + List tokens = new LinkedList(); } { - { createView.setOrReplace(isUsingOrReplace);} - [ - { createView.setForce(ForceOption.NO_FORCE); } - | { createView.setForce(ForceOption.FORCE); } - ] - [ { createView.setSecure(true);} ] - [ - { createView.setTemporary(TemporaryOption.TEMP); } - | { createView.setTemporary(TemporaryOption.TEMPORARY); } - | { createView.setTemporary(TemporaryOption.VOLATILE); } - ] - [ { createView.setMaterialized(true);} ] - view=Table() { createView.setView(view); } - [LOOKAHEAD(3) (tk= | tk=) { createView.setAutoRefresh(AutoRefreshOption.from(tk.image)); } ] - [LOOKAHEAD(3) {createView.setIfNotExists(true);}] - [ columnNames=ColumnWithCommentList( ) { createView.setColumnNames(columnNames); } ] - [ commentTokens=CreateViewTailComment( ) { createView.setViewCommentOptions(commentTokens); } ] - - select=Select( ) { createView.setSelect(select); } - [ LOOKAHEAD(2) { createView.setWithReadOnly(true); } ] - { return createView; } -} -List CreateViewTailComment(): + ( + LOOKAHEAD({ isKeywordAhead("OPTIONS") || getToken(1).kind == K_ALTER + && "OPTIONS".equalsIgnoreCase(getToken(getToken(2).kind == K_COLUMN ? 4 : 3).image) }) + alterExp=AlterForeignDataOptions() + | + LOOKAHEAD( ) alterExp=MySqlAlterOrderBy() + | + LOOKAHEAD({ isMySqlTableOptionAhead() }) + alterExp=AlterSharedTableOption() + | + LOOKAHEAD({ getToken(1).kind == K_ALTER && getToken(2).kind == K_CONSTRAINT + && isPostgreSqlConstraintAttributeAhead(4) }) alterExp=AlterConstraintAttributes() + | + LOOKAHEAD({ isPostgreSqlRelationActionAhead() }) alterExp=PostgreSqlRelationAction() { + requireDdlSyntax(((RelationAlterAction) alterExp).getColumnNumber() == null, + "ALTER TABLE expects a column name, not an ordinal"); + } + | + LOOKAHEAD( , + { Dialect.COCKROACHDB.name().equals(getAsString(Feature.dialect)) }) + alterExp = CockroachAlterPrimaryKey() + | + alterExp = AlterExpressionAddAlterModify() + | + ( + { alterExp = new AlterExpression(); } + { alterExp.setOperation(AlterOperation.CHANGE); } + [ { alterExp.hasColumn(true); alterExp.setOptionalSpecifier("COLUMN"); } ] + ( + { AlterExpression.ColumnDataType alterExpressionColumnDataType; } + (tk=KeywordOrIdentifier()) + alterExpressionColumnDataType = AlterExpressionColumnDataType() { alterExp.withColumnOldName(tk.image).addColDataType(alterExpressionColumnDataType); } + ) + ) + | + alterExp = AlterExpressionDrop() + | + LOOKAHEAD(5) ( + { alterExp = new AlterExpression(); } + { + alterExp.setOperation(AlterOperation.FORCE_ROW_LEVEL_SECURITY); + } + ) + | + LOOKAHEAD(5) ( + { alterExp = new AlterExpression(); } + { + alterExp.setOperation(AlterOperation.NO_FORCE_ROW_LEVEL_SECURITY); + } + ) + | + LOOKAHEAD(1) ( + { alterExp = new AlterExpression(); } + { alterExp.setOperation(AlterOperation.FORCE); } + ) + | + ( + { alterExp = new AlterExpressionTableOption(); } + { alterExp.setOperation(AlterOperation.ALGORITHM); } + ["=" { alterExp.setUseEqual(true);} ] + sk3 = RelObjectName() { alterExp.setAlgorithmOption(sk3); } + ) + | + ( + { alterExp = new AlterExpressionTableOption(); } + { alterExp.setOperation(AlterOperation.LOCK); } + ["=" { alterExp.setUseEqual(true);} ] + sk3 = RelObjectName() { alterExp.setLockOption(sk3); } + ) + | + LOOKAHEAD(2) alterExp = AlterExpressionRenameOp() + | + ({ alterExp = new AlterExpressionCharset(); } + { + alterExp.setOperation(AlterOperation.CONVERT); + alterExp.setConvertType(AlterExpression.ConvertType.CONVERT_TO); + } + + ( + | LOOKAHEAD({ isKeywordAhead("CHARSET") }) { alterExp.setUseCharsetKeyword(true); } ) + ( tk= | tk=CharacterSetName() ) { alterExp.setCharacterSet(tk.image); } + [ LOOKAHEAD(2) tk2=CharacterSetName() { alterExp.setCollation(tk2.image); }] + ) + | + alterExp = AlterExpressionDiscardOrImport() + | + alterExp = AlterExpressionPostgreSqlPartitionOp() + | + LOOKAHEAD(4) ( + { alterExp = new AlterExpression(); } + { + alterExp.setOperation(AlterOperation.DISABLE_ROW_LEVEL_SECURITY); + } + ) + | + LOOKAHEAD(2) ( + { alterExp = new AlterExpressionTableOption(); } + { + alterExp.setOperation(AlterOperation.DISABLE_KEYS); + } + ) + | + LOOKAHEAD(4) ( + { alterExp = new AlterExpression(); } + { + alterExp.setOperation(AlterOperation.ENABLE_ROW_LEVEL_SECURITY); + } + ) + | + LOOKAHEAD(2) ( + { alterExp = new AlterExpressionTableOption(); } + { + alterExp.setOperation(AlterOperation.ENABLE_KEYS); + } + ) + | + alterExp = AlterExpressionPartitionOp() + | + { alterExp = new AlterExpression(); } + tokens = captureRest() { + alterExp.setOperation(AlterOperation.UNSPECIFIC); + StringBuilder optionalSpecifier = new StringBuilder(); + int i=0; + + for (String s: tokens) + if (! (s.equals(";") || s.equals("\n\n\n")) ) { + if (i>0) + optionalSpecifier.append( " " ); + optionalSpecifier.append( s ); + i++; + } + + requireDdlSyntax(i > 0, "Expected an ALTER TABLE action"); + alterExp.setOptionalSpecifier( optionalSpecifier.toString() ); + } + ) + + { + return alterExp; + } +} +Statement Alter(): { - Token tk = null; - Token tk2 = null; - String op = null; - List result = new ArrayList(); + Statement statement; + List captureRest; } { - tk= - [ "=" { op = "="; } ] - tk2 = { - result.add(""); - result.add(tk.image); - if (op != null) { - result.add(op); - } - result.add(tk2.image); + ( + ( + + ( + LOOKAHEAD( ) statement = AlterForeignDataWrapper() + | + LOOKAHEAD({ isKeywordAhead("SERVER") }) statement = AlterServer() + | + LOOKAHEAD({ getToken(1).kind == K_USER && "MAPPING".equalsIgnoreCase(getToken(2).image) }) statement = AlterUserMapping() + | + LOOKAHEAD({ getToken(1).kind == K_TEXT_LITERAL && getToken(2).kind == K_SEARCH + && "CONFIGURATION".equalsIgnoreCase(getToken(3).image) }) + statement = AlterTextSearchConfiguration() + | + LOOKAHEAD({ isKeywordAhead("COLLATION") }) statement = AlterCollation() + | + LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = AlterStatistics() + | + // Other routine forms, such as SQL Server definitions, remain unsupported. + LOOKAHEAD({ isAlterRoutineAhead() }) statement = AlterRoutine() + | + LOOKAHEAD(AlterTrigger() ( | )) statement = AlterTrigger() + | + LOOKAHEAD(1, ) statement = AlterPolicy() + | + LOOKAHEAD({ isMySqlAlterUserAhead() }) statement = AlterUser() + | + LOOKAHEAD({ isKeywordAhead("ROLE") || isKeywordAhead("USER") || isKeywordAhead("GROUP") }) + statement = AlterRole() + | + statement = AlterDefaultPrivileges() + | + LOOKAHEAD({ (getToken(1).kind == K_DATABASE || getToken(1).kind == K_SCHEMA) + && Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) }) + statement = AlterDatabase() + | + // Other dialects keep unmodelled ALTER DATABASE forms as unsupported statements. + LOOKAHEAD(AlterDatabase()) statement = AlterDatabase() + | + LOOKAHEAD(1, ) statement = AlterSchema() + | + LOOKAHEAD(1, ) statement = AlterType() + | + LOOKAHEAD(1, ) statement = AlterDomain() + | + LOOKAHEAD({ isKeywordAhead("EXTENSION") }) statement = AlterExtension() + | + LOOKAHEAD({ isKeywordAhead("PUBLICATION") }) statement = AlterPublication() + | + LOOKAHEAD({ isKeywordAhead("SUBSCRIPTION") }) statement = AlterSubscription() + | + LOOKAHEAD({ isPostgreSqlEventTriggerAhead(true) }) statement = AlterEventTrigger() + | + LOOKAHEAD([ MySqlRoutineDefiner() ] , + { getToken(1).kind == K_EVENT || isKeywordAhead("DEFINER") }) + statement = AlterEvent() + | + LOOKAHEAD(( | | ) ) + statement = PostgreSqlTablespaceMove() + | + // other ALTER TABLESPACE forms, such as Oracle data file clauses, remain unsupported + LOOKAHEAD(AlterTablespace() ( | )) + statement = AlterTablespace() + | + statement = AlterForeignTable() + | + statement = AlterTable(false) + | + statement = AlterSession() + | + LOOKAHEAD(MySqlViewPrefix() Table() [ ColumnsNamesList() ] ) statement = AlterView(false) + | + LOOKAHEAD({ getToken(1).kind == K_INDEX || getToken(1).kind == K_MATERIALIZED + || getToken(1).kind == K_VIEW }) statement=PostgreSqlAlterRelation() + | + statement = AlterSystemStatement() + | + statement = AlterSequence() + | + captureRest = captureRest() + { + statement = new UnsupportedStatement("ALTER", captureRest); + } + ) + ) + | + ( + + ( + statement = AlterView(true) + | + captureRest = captureRest() + { + statement = new UnsupportedStatement("REPLACE", captureRest); + } + ) + ) + ) + { + return statement; } - { return result;} } - -ReferentialAction.Action Action(): +AlterTablespaceMove PostgreSqlTablespaceMove(): { - ReferentialAction.Action action = null; + AlterTablespaceMove result = new AlterTablespaceMove(); + String name; } { - ( { action=ReferentialAction.Action.CASCADE;} - | - {action=ReferentialAction.Action.RESTRICT;} - | - {action=ReferentialAction.Action.NO_ACTION;} - | - ( - {action=ReferentialAction.Action.SET_NULL;} - | - {action=ReferentialAction.Action.SET_DEFAULT;} - ) - ) - { return action; } + ( { result.setObjectType(AlterTablespaceMove.ObjectType.TABLE); } + | { result.setObjectType(AlterTablespaceMove.ObjectType.INDEX); } + | { result.setObjectType(AlterTablespaceMove.ObjectType.MATERIALIZED_VIEW); } ) + name=RelObjectName() { result.setSourceTablespace(name); } + [ ContextualKeyword("OWNED") name=RelObjectName() { result.getOwners().add(name); } + ( "," name=RelObjectName() { result.getOwners().add(name); } )* ] + name=RelObjectName() { result.setTargetTablespace(name); } + [ { result.setNoWait(true); } ] + { return result; } } /** - * Parses optional referential actions: [ON DELETE|UPDATE action] [ON DELETE|UPDATE action] - * Shared between CREATE TABLE FK and ALTER TABLE FK definitions. + * MySQL CREATE [UNDO] TABLESPACE with InnoDB options, or PostgreSQL CREATE TABLESPACE with an + * owner, a location and tablespace parameters. */ -void ReferentialActionsOnIndex(ForeignKeyIndex fkIndex): +CreateTablespace CreateTablespace(): { - Token tk; - ReferentialAction.Action action = null; + CreateTablespace statement = new CreateTablespace(); + String name; + Token token; + StringValue dataFile; + List parameters; + List options; } { - [ LOOKAHEAD(2) ( - - ( tk= | tk= ) action = Action() - { fkIndex.setReferentialAction(ReferentialAction.Type.from(tk.image), action); } - )] - [ LOOKAHEAD(2) ( - - ( tk= | tk= ) action = Action() - { fkIndex.setReferentialAction(ReferentialAction.Type.from(tk.image), action); } - )] + ( + LOOKAHEAD({ isKeywordAhead("UNDO") }) ContextualKeyword("UNDO") + name=RelObjectName() { statement.setUndo(true); statement.setName(name); } + dataFile=MySqlTablespaceDataFile() { statement.setDataFile(dataFile); } + [ LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) }) options=MySqlTablespaceOptions(true, true) + { statement.setOptions(options); } ] + | + name=RelObjectName() { statement.setName(name); } + ( + LOOKAHEAD({ isKeywordAhead("OWNER") || isKeywordAhead("LOCATION") }) + [ LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") + name=RelObjectName() { statement.setOwner(name); } ] + ContextualKeyword("LOCATION") token= + { statement.setLocation(stringValue(token)); } + [ LOOKAHEAD( "(") parameters=PostgreSqlIndexOptions() + { statement.setParameters(parameters); } ] + | + [ dataFile=MySqlTablespaceDataFile() { statement.setDataFile(dataFile); } ] + [ LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) }) options=MySqlTablespaceOptions(true, false) + { statement.setOptions(options); } ] + ) + ) + { return statement; } } -/** - * Parses: CHECK ( expression ) - * Returns a CheckConstraint. Shared between CREATE TABLE and ALTER TABLE. - */ -CheckConstraint CheckConstraintSpec(String constraintName): -{ - Expression exp = null; -} +StringValue MySqlTablespaceDataFile(): +{ Token token; } { - ( LOOKAHEAD(2) "(" exp = Expression() ")" )* - { - return new CheckConstraint().withName(constraintName).withExpression(exp); - } + ContextualKeyword("DATAFILE") token= { return stringValue(token); } } /** - * Parses: FOREIGN KEY columns REFERENCES table [columns] [referential actions] - * Returns a populated ForeignKeyIndex. Shared between CREATE TABLE and ALTER TABLE. + * MySQL ALTER [UNDO] TABLESPACE and PostgreSQL ALTER TABLESPACE. Alter() selects this production + * only when it matches the whole statement. */ -ForeignKeyIndex ForeignKeySpec(String constraintName): +AlterTablespace AlterTablespace(): { - ForeignKeyIndex fkIndex = new ForeignKeyIndex(); - Token tk; - Token tk2; - List refColNames = null; - List colNames; - Table fkTable; + AlterTablespace statement = new AlterTablespace(); + String name; + List parameters; + List names = new ArrayList(); + List options; } { - tk= tk2= - colNames = ColumnNamesWithParamsList() - { - if (constraintName != null) { fkIndex.setName(constraintName); } - fkIndex.withType(tk.image + " " + tk2.image).withColumns(colNames); - } - fkTable=Table() [ LOOKAHEAD(2) refColNames=ColumnsNamesList() ] - { - fkIndex.setTable(fkTable); - fkIndex.setReferencedColumnNames(refColNames); - } - ReferentialActionsOnIndex(fkIndex) - { - return fkIndex; - } + ( + LOOKAHEAD({ isKeywordAhead("UNDO") }) ContextualKeyword("UNDO") + name=RelObjectName() { statement.setUndo(true); statement.setName(name); } + + ( + { statement.setState(AlterTablespace.State.ACTIVE); } + | LOOKAHEAD({ isKeywordAhead("INACTIVE") }) ContextualKeyword("INACTIVE") + { statement.setState(AlterTablespace.State.INACTIVE); } + ) + { statement.setAction(AlterTablespace.Action.SET_STATE); } + [ LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) }) options=MySqlTablespaceOptions(false, true) + { statement.setOptions(options); } ] + | + name=RelObjectName() { statement.setName(name); } + ( + name=RelObjectName() + { statement.setAction(AlterTablespace.Action.RENAME); statement.setNewName(name); } + | + LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterTablespace.Action.OWNER); statement.setOwner(name); } + | + parameters=PostgreSqlIndexOptions() + { statement.setAction(AlterTablespace.Action.SET_PARAMETERS); statement.setParameters(parameters); } + | + "(" name=PostgreSqlStorageParameterName() { names.add(name); } + ( "," name=PostgreSqlStorageParameterName() { names.add(name); } )* ")" + { statement.setAction(AlterTablespace.Action.RESET_PARAMETERS); statement.setResetParameters(names); } + | + LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) }) options=MySqlTablespaceOptions(false, false) + { statement.setAction(AlterTablespace.Action.OPTIONS); statement.setOptions(options); } + ) + ) + { return statement; } } /** - * Parses USING [INDEX] name clause in ALTER TABLE constraint tails. + * MySQL tablespace options in source order, optionally separated by commas. Each clause accepts + * only the options that it defines. */ -void AlterExpressionUsingIndex(AlterExpression alterExp): +List MySqlTablespaceOptions(boolean create, boolean undo): { - String sk4; + List options = new ArrayList(); + TableOption option; } { - { alterExp.addParameters("USING"); } - [ LOOKAHEAD(2) { alterExp.addParameters("INDEX"); } ] - sk4=RelObjectName() { alterExp.addParameters(sk4); } + option=MySqlTablespaceOption() { options.add(validateTablespaceOption(option, create, undo)); } + ( + LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) + || getToken(1).kind == K_COMMA && isMySqlTablespaceOptionAhead(2) }) + [ "," ] option=MySqlTablespaceOption() + { options.add(validateTablespaceOption(option, create, undo)); } + )* + { return options; } } -/** - * Parses the common tail after a named constraint in ALTER TABLE: - * constraint_state [USING [INDEX] name] [COMMENT 'text'] - */ -void AlterExpressionConstraintTail(AlterExpression alterExp, Index index): +/** Reuses the table option models for ENGINE, ENCRYPTION, AUTOEXTEND_SIZE and ENGINE_ATTRIBUTE. */ +TableOption MySqlTablespaceOption(): { - List constraints; + TableOption option; + String value; + boolean useEquals = false; } { - constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } - [ LOOKAHEAD(2) AlterExpressionUsingIndex(alterExp) ] - [ LOOKAHEAD(2) index = IndexWithComment(index) { alterExp.setIndex(index); } ] + ( + LOOKAHEAD({ isKeywordAhead("FILE_BLOCK_SIZE") }) ContextualKeyword("FILE_BLOCK_SIZE") + [ "=" { useEquals = true; } ] value=MySqlSizeNumber() + { option = new TableOption(TableOption.Kind.FILE_BLOCK_SIZE, "FILE_BLOCK_SIZE", value, useEquals); } + | + LOOKAHEAD({ getToken(1).kind == K_ENGINE }) option=MySqlTableOption() + | + option=MySqlScalarTableOption() + ) + { return option; } } -AlterView AlterView(boolean useReplace): +/** The new relation statements reuse the same actions as structured ALTER TABLE properties. */ +AlterRelation PostgreSqlAlterRelation(): { - AlterView alterView = new AlterView(); - Table view = null; - Select select = null; - List columnNames = null; + AlterRelation result = new AlterRelation(); + Table relation; + RelationAlterAction action; } { - view=Table() { alterView.setView(view); alterView.setUseReplace(useReplace); } - [ columnNames = ColumnsNamesList() { alterView.setColumnNames(columnNames); } ] - - select=Select() + ( { result.setObjectType(AlterRelation.ObjectType.INDEX); } + | { result.setObjectType(AlterRelation.ObjectType.VIEW); } + | { result.setObjectType(AlterRelation.ObjectType.MATERIALIZED_VIEW); } ) + [ LOOKAHEAD(2) { result.setIfExists(true); } ] + relation=Table() { result.setRelation(relation); } + action=PostgreSqlRelationAction() { + validateRelationAction(result.getObjectType(), action); result.getActions().add(action); + } + ( "," action=PostgreSqlRelationAction() { + validateRelationAction(result.getObjectType(), action); result.getActions().add(action); + } )* { - alterView.setSelect(select); - return alterView; + if (result.getActions().size() > 1) { + for (RelationAlterAction item : result.getActions()) { + requireDdlSyntax(!EnumSet.of(RelationAlterAction.Kind.RENAME, + RelationAlterAction.Kind.RENAME_COLUMN, RelationAlterAction.Kind.SET_SCHEMA, + RelationAlterAction.Kind.ATTACH_PARTITION, RelationAlterAction.Kind.DEPENDS_ON_EXTENSION) + .contains(item.getKind()), "This relation action must be a separate statement"); + } + } + return result; } } -List CreateParameter(): +/** Shared state syntax; each caller applies its own target restrictions. */ +RelationAlterAction.EnableState PostgreSqlEnableState(): +{ RelationAlterAction.EnableState state; } { - String retval = ""; - Token tk = null, tk2 = null; - Expression exp = null; - ColDataType colDataType; - List param = new ArrayList(); + ( { state = RelationAlterAction.EnableState.DISABLE; } + | { state = RelationAlterAction.EnableState.ENABLE; } + [ { state = RelationAlterAction.EnableState.ENABLE_ALWAYS; } + | LOOKAHEAD({ isKeywordAhead("REPLICA") }) ContextualKeyword("REPLICA") + { state = RelationAlterAction.EnableState.ENABLE_REPLICA; } ] ) + { return state; } +} + +RelationAlterAction PostgreSqlRelationAction(): +{ + RelationAlterAction action = new RelationAlterAction(); + String name; + Token token; + Table relation; + Expression expression; + Long statistics; + RelationAlterAction.EnableState enableState; + List options; + List names = new ArrayList(); } { ( - // Postgres: nextval('public.actor_actor_id_seq'::regclass) - ( "(" tk= "::" colDataType = ColDataType() ")" ) - { - param.add("NextVal( " + tk.image + "::" + colDataType + ")" ); - } + LOOKAHEAD({ isKeywordAhead("CLUSTER") }) ContextualKeyword("CLUSTER") name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.CLUSTER_ON); action.setValue(name); } | - ( - //@todo: implement a proper identifier - (tk= | tk= | tk=) - { retval+=tk.image; } - - [ - "." - //@todo: implement a proper identifier - (tk2= | tk2= | tk=) - { retval+="."+tk2.image; } - ] - { param.add(retval); } - ) + name=AccessQualifiedName() + { action.setKind(RelationAlterAction.Kind.OF); action.setValue(name); } | - LOOKAHEAD(3) [ { param.add("USING INDEX"); }] retval=RelObjectName() - { param.add("TABLESPACE " + retval); } + { action.setKind(RelationAlterAction.Kind.NOT_OF); } | + enableState=PostgreSqlEnableState() { action.setEnableState(enableState); } ( - tk= | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= | tk= | tk = | tk = - | tk= | tk= | tk= | tk= | tk= - | tk= | tk= - | tk="=" + + ( LOOKAHEAD(1) { action.setTriggerTarget(RelationAlterAction.TriggerTarget.ALL); } + | LOOKAHEAD(1) { action.setTriggerTarget(RelationAlterAction.TriggerTarget.USER); } + | name=RelObjectName() { action.setTriggerTarget(RelationAlterAction.TriggerTarget.NAME); action.setValue(name); } ) + { + action.setKind(RelationAlterAction.Kind.TRIGGER_STATE); + requireDdlSyntax(action.getTriggerTarget() == RelationAlterAction.TriggerTarget.NAME + || enableState == RelationAlterAction.EnableState.ENABLE + || enableState == RelationAlterAction.EnableState.DISABLE, + "ENABLE ALWAYS/REPLICA requires a trigger name"); + } + | ContextualKeyword("RULE") name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.RULE_STATE); action.setValue(name); } ) - { param.add(tk.image); } | - ( tk= | tk= | tk= ) [ LOOKAHEAD(2) "(" exp = Expression() ")" ] - { - param.add(tk.image); - if (exp!=null) { - param.add("(" + exp + ")"); + + ( + LOOKAHEAD(1) name=RelObjectName() { + action.setKind(RelationAlterAction.Kind.RENAME); action.setNewName(name); } - } + | + [ LOOKAHEAD(1) { action.hasColumn(true); } ] + name=RelObjectName() { action.setColumnName(name); } + name=RelObjectName() { + action.setKind(RelationAlterAction.Kind.RENAME_COLUMN); action.setNewName(name); + } + ) + | + LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.OWNER); action.setValue(name); } | + ( - [ ( tk="+" | tk="-" ) { retval = tk.image; } ] - tk= | tk= + + ( LOOKAHEAD({ isKeywordAhead("CLUSTER") }) ContextualKeyword("CLUSTER") + { action.setKind(RelationAlterAction.Kind.SET_WITHOUT_CLUSTER); } + | ContextualKeyword("OIDS") { action.setKind(RelationAlterAction.Kind.SET_WITHOUT_OIDS); } ) + | { action.setKind(RelationAlterAction.Kind.SET_UNLOGGED); } + | LOOKAHEAD({ isKeywordAhead("LOGGED") }) ContextualKeyword("LOGGED") + { action.setKind(RelationAlterAction.Kind.SET_LOGGED); } + | name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.SET_SCHEMA); action.setValue(name); } + | name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.SET_TABLESPACE); action.setValue(name); } + | LOOKAHEAD({ isKeywordAhead("ACCESS") }) ContextualKeyword("ACCESS") ContextualKeyword("METHOD") name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.SET_ACCESS_METHOD); action.setValue(name); } + | options=PostgreSqlIndexOptions() + { action.setKind(RelationAlterAction.Kind.SET_OPTIONS); action.setOptions(options); } ) - { param.add( retval + tk.image ); } | - retval=AList() { param.add(retval); } + "(" name=IndexKeyAttributeName() { names.add(name); } + ( "," name=IndexKeyAttributeName() { names.add(name); } )* ")" + { action.setKind(RelationAlterAction.Kind.RESET_OPTIONS); action.setResetOptions(names); } | - (tk= tk2=) { param.add(tk.image); param.add(tk2.image);} + LOOKAHEAD( ) name=RelObjectName() + [ { action.setNoInherit(true); } ] ContextualKeyword("INHERIT") + { action.setKind(RelationAlterAction.Kind.ALTER_CONSTRAINT_INHERIT); action.setConstraintName(name); } | - ( exp=ArrayConstructor(true)) { param.add(exp.toString()); } + [ LOOKAHEAD(1) { action.hasColumn(true); } ] + ( token= { action.setColumnNumber(Integer.valueOf(token.image)); } + | name=RelObjectName() { action.setColumnName(name); } ) + { action.setKind(RelationAlterAction.Kind.ALTER_COLUMN); } + ( + + ( + expression=Expression() { + action.setColumnAction(RelationAlterAction.ColumnAction.SET_DEFAULT); + action.setDefaultExpression(expression); + } + | + LOOKAHEAD({ isKeywordAhead("EXPRESSION") }) ContextualKeyword("EXPRESSION") + expression=ColumnGenerationExpression() { + action.setColumnAction(RelationAlterAction.ColumnAction.SET_EXPRESSION); + action.setGenerationExpression(expression); + } + | + LOOKAHEAD({ isKeywordAhead("STATISTICS") }) ContextualKeyword("STATISTICS") + { action.setColumnAction(RelationAlterAction.ColumnAction.SET_STATISTICS); } + ( { action.setStatisticsDefault(true); } + | statistics=SequenceParameterValue() { + requireDdlSyntax(statistics >= -1 && statistics <= 10000, "Statistics target must be between -1 and 10000"); + action.setStatistics(statistics); + } ) + | + LOOKAHEAD(1, "(") options=PostgreSqlIndexOptions() { + for (IndexOption option : options) { + requireDdlSyntax(option.getValue() != null && option.isUseEquals(), + "Column attributes require a value and an equals sign"); + } + action.setColumnAction(RelationAlterAction.ColumnAction.SET_OPTIONS); action.setOptions(options); + } + | + ( token= | token= | token= ) { + requireDdlSyntax(Arrays.asList("PLAIN", "EXTERNAL", "EXTENDED", "MAIN", "DEFAULT") + .contains(token.image.toUpperCase(Locale.ROOT)), "Invalid column storage strategy"); + action.setColumnAction(RelationAlterAction.ColumnAction.SET_STORAGE); action.setValue(token.image); + } + | + ContextualKeyword("COMPRESSION") name=RelObjectName() + { action.setColumnAction(RelationAlterAction.ColumnAction.SET_COMPRESSION); action.setValue(name); } + ) + | + "(" name=RelObjectName() { names.add(name); } + ( "," name=RelObjectName() { names.add(name); } )* ")" + { action.setColumnAction(RelationAlterAction.ColumnAction.RESET_OPTIONS); action.setResetOptions(names); } + | + + ( + { action.setColumnAction(RelationAlterAction.ColumnAction.DROP_DEFAULT); } + | + ContextualKeyword("EXPRESSION") { action.setColumnAction(RelationAlterAction.ColumnAction.DROP_EXPRESSION); } + [ LOOKAHEAD(2) { action.setUsingIfExists(true); } ] + ) + ) | - tk="::" colDataType = ColDataType() { param.add(tk.image); param.add(colDataType.toString()); } + relation=Table() + { action.setKind(RelationAlterAction.Kind.ATTACH_PARTITION); action.setRelation(relation); } + | + LOOKAHEAD({ isKeywordAhead("DEPENDS") || getToken(1).kind == K_NO && "DEPENDS".equalsIgnoreCase(getToken(2).image) }) + [ { action.setNoDependency(true); } ] ContextualKeyword("DEPENDS") ContextualKeyword("EXTENSION") name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.DEPENDS_ON_EXTENSION); action.setValue(name); } + | + name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.VALIDATE_CONSTRAINT); action.setConstraintName(name); } + | + LOOKAHEAD({ isKeywordAhead("REPLICA") }) ContextualKeyword("REPLICA") + { action.setKind(RelationAlterAction.Kind.REPLICA_IDENTITY); } + ( + name=RelObjectName() + { action.setReplicaIdentity(RelationAlterAction.ReplicaIdentity.USING_INDEX); action.setValue(name); } + | ( token= | token= | token= | token= ) + { action.setReplicaIdentity(accessEnum(RelationAlterAction.ReplicaIdentity.class, token.image)); } + ) + | + [ { action.setNoInherit(true); } ] ContextualKeyword("INHERIT") relation=Table() + { action.setKind(RelationAlterAction.Kind.INHERIT); action.setRelation(relation); } ) - {return param;} + { return action; } } -// row_movement_clause https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_7002.htm#i2204697 -RowMovement RowMovement(): +AlterForeignDataOptions AlterForeignDataOptions(): { - RowMovement rowMovement = new RowMovement(); + AlterForeignDataOptions action = new AlterForeignDataOptions(); + String name; + List options; } { - ( - ( ) { rowMovement.setMode(RowMovementMode.ENABLE); } - | - ( ) { rowMovement.setMode(RowMovementMode.DISABLE); } - ) - { return rowMovement;} + [ [ LOOKAHEAD(1) { action.hasColumn(true); } ] + name=RelObjectName() { action.setColumnName(name); } ] + options=ForeignDataOptions(true) { action.setOptions(options); } + { return action; } } -String AList(): -{ - StringBuilder retval = new StringBuilder("("); - Token tk = null; - String name = null; -} +Alter AlterForeignTable(): +{ Alter alter; } { - "(" - - ( - ( (tk= | tk= | tk= | tk= | tk=) { retval.append(tk.image); } - | (name=RelObjectName()) { retval.append(name); }) - [("," {retval.append(",");} | "=" {retval.append("=");})] )* - - ")" - { - retval.append(")"); - return retval.toString(); - } + alter=AlterTable(true) { alter.setForeignTable(true); return alter; } } -String ColumnsNamesListItem(): +Alter AlterTable(boolean foreign): { - Token tk = null; - Token sortDirection = null; - String item = null; + Alter alter = new Alter(); + Table table; + AlterExpression alterExp; + boolean usingIfExists = false; } { - ( item = RelObjectName() ) - [ LOOKAHEAD(2) "(" tk = ")" { item = item + "(" + tk.image + ")"; } ] - [ LOOKAHEAD( ( | ) ( "," | ")" ) ) (sortDirection = | sortDirection = ) { item = item + " " + sortDirection.image; } ] - { - return item; - } -} + { alter.setForeignTable(foreign); } + [ LOOKAHEAD(2) { alter.setUseTableIfExists(true); } ] + [ LOOKAHEAD(2) { alter.setUseOnly(true); } ] + [ LOOKAHEAD(2) { + requireDdlSyntax(!alter.isUseTableIfExists(), "Duplicate IF EXISTS"); + alter.setUseTableIfExists(true); + } ] + table=Table() { alter.setTable(table); } + [ "*" { + requireDdlSyntax(!alter.isUseOnly(), "ONLY cannot be combined with descendant marker"); + alter.setUseDescendants(true); + } ] -List ColumnsNamesList(): -{ - List retval = new ArrayList(); - String img = null; -} -{ - "(" - img=ColumnsNamesListItem() { retval.add(img); } - ( "," img=ColumnsNamesListItem() { retval.add(img); } )* + alterExp=AlterExpression() { alter.addAlterExpression(alterExp); } + + ( + "," alterExp=AlterExpression() { alter.addAlterExpression(alterExp); } + | LOOKAHEAD({ isMySqlTableOptionAhead() }) + { requireDdlSyntax(isMySqlTableOptionAction(alterExp), "Expected a comma before table options"); } + alterExp=AlterSharedTableOption() { alter.addAlterExpression(alterExp); } + )* + + // MySQL partition_options end the action list without a comma + [ + LOOKAHEAD({ isTrailingPartitionOptionAhead() }) + { + requireDdlSyntax(!Alter.isTrailingPartitionOption(alterExp), + "PARTITION BY and REMOVE PARTITIONING cannot follow each other"); + } + alterExp=AlterExpressionPartitionOp() { alter.addAlterExpression(alterExp); } + ] - ")" { - return retval; + for (AlterExpression action : alter.getAlterExpressions()) { + requireDdlSyntax(foreign || !(action instanceof AlterForeignDataOptions), "OPTIONS requires ALTER FOREIGN TABLE"); + } + return alter; } } -String FuncArgsListItem(): +AlterSession AlterSession(): { - Token tk = null; - String argName = null; - String argType = null; + AlterSessionOperation operation = null; + List parameters = new ArrayList(); + Token token; } { - ( - LOOKAHEAD(2) ( - argName = RelObjectName() - argType = RelObjectName() - [ "(" tk = ")" { argType = argType + "(" + tk.image + ")"; } ] + ( + ( + ( { operation = AlterSessionOperation.ADVISE_COMMIT; } + | { operation = AlterSessionOperation.ADVISE_ROLLBACK; } + | { operation = AlterSessionOperation.ADVISE_NOTHING; } + ) + ) + | + ( + { operation = AlterSessionOperation.CLOSE_DATABASE_LINK; } + ) + | + ( + ( { operation = AlterSessionOperation.ENABLE_COMMIT_IN_PROCEDURE; } + | { operation = AlterSessionOperation.ENABLE_GUARD; } + | ( { operation = AlterSessionOperation.ENABLE_PARALLEL_DML; } + | { operation = AlterSessionOperation.ENABLE_PARALLEL_DDL; } + | { operation = AlterSessionOperation.ENABLE_PARALLEL_QUERY; } + ) + | { operation = AlterSessionOperation.ENABLE_RESUMABLE; } + ) + ) + | + ( + ( { operation = AlterSessionOperation.DISABLE_COMMIT_IN_PROCEDURE; } + | { operation = AlterSessionOperation.DISABLE_GUARD; } + | ( { operation = AlterSessionOperation.DISABLE_PARALLEL_DML; } + | { operation = AlterSessionOperation.DISABLE_PARALLEL_DDL; } + | { operation = AlterSessionOperation.DISABLE_PARALLEL_QUERY; } + ) + | { operation = AlterSessionOperation.DISABLE_RESUMABLE; } + ) + ) + | + ( + ( { operation = AlterSessionOperation.FORCE_PARALLEL_DML; } + | { operation = AlterSessionOperation.FORCE_PARALLEL_DDL; } + | { operation = AlterSessionOperation.FORCE_PARALLEL_QUERY; } + ) ) | ( - argType = RelObjectName() - [ "(" tk = ")" { argType = argType + "(" + tk.image + ")"; } ] + { operation = AlterSessionOperation.SET; } ) ) - { - return argName != null ? String.format("%s %s", argName, argType) : argType; - } -} -List FuncArgsList(): -{ - List retval = null; - String img = null; -} -{ - "(" - { retval = new ArrayList(); } - [ - img=FuncArgsListItem() { retval.add(img); } - ( "," img=FuncArgsListItem() { retval.add(img); } )* - ] - ")" + ( ( token = + | token = + | token = "=" + | token = + | token = + ) { parameters.add( token.image ); } + )* + + { - return retval; + return new AlterSession(operation, parameters); } } -Drop Drop(): +AlterSystemStatement AlterSystemStatement(): { - Drop drop = new Drop(); - Token tk = null; - Table name; - List dropArgs = new ArrayList(); - List funcArgs = null; - boolean useTemporary = false; + AlterSystemOperation operation = null; + List parameters = new LinkedList(); } { - - [ { drop.setMaterialized(true);} ] - ( - tk= + ( + ( + { operation = AlterSystemOperation.ARCHIVE_LOG; } + ) | ( - [ {useTemporary=true;} ] tk= + { operation = AlterSystemOperation.CHECKPOINT; } ) | - tk= + ( + { operation = AlterSystemOperation.DUMP_ACTIVE_SESSION_HISTORY; } + ) | - tk= + ( + ( + ContextualKeyword("DISTRIBUTED") ContextualKeyword("RECOVERY") { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; } + | { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; } + ) + ) | - tk= + ( + ( + ContextualKeyword("DISTRIBUTED") ContextualKeyword("RECOVERY") { operation = AlterSystemOperation.DISABLE_DISTRIBUTED_RECOVERY; } + | { operation = AlterSystemOperation.DISABLE_RESTRICTED_SESSION; } + ) + ) | - tk= + ( + { operation = AlterSystemOperation.FLUSH; } + ) + | + ( + { operation = AlterSystemOperation.DISCONNECT_SESSION; } + ) + | + ( + { operation = AlterSystemOperation.KILL_SESSION; } + ) + | + ( + { operation = AlterSystemOperation.SWITCH; } + ) + | + ( + { operation = AlterSystemOperation.SUSPEND; } + ) + | + ( + { operation = AlterSystemOperation.RESUME; } + ) + | + ( + { operation = AlterSystemOperation.QUIESCE; } + ) + | + ( + { operation = AlterSystemOperation.UNQUIESCE; } + ) + | + ( + { operation = AlterSystemOperation.SHUTDOWN; } + ) + | + ( + { operation = AlterSystemOperation.REGISTER; } + ) | - tk= - ) - { drop.setType(tk.image); } - - [ LOOKAHEAD(2) {drop.setIfExists(true);} ] - - name = Table() { drop.setName(name); } - [ LOOKAHEAD(2) funcArgs = FuncArgsList() ] - ( ( - tk= | tk= | tk= - ) { dropArgs.add(tk.image); } + { operation = AlterSystemOperation.SET; } + ) | ( - name = Table() { dropArgs.add("ON"); dropArgs.add(name.toString()); } + { operation = AlterSystemOperation.RESET; } ) - )* - { - if (dropArgs.size() > 0) { - drop.setParameters(dropArgs); - } - if (drop.getType().equals("FUNCTION")) { - drop.getTypeToParameters().put("FUNCTION", funcArgs); - } - - drop.setUsingTemporary(useTemporary); + ) + parameters = captureRest() - return drop; + { + return new AlterSystemStatement(operation, parameters); } } -Truncate Truncate(): +Wait Wait(): { - Truncate truncate = new Truncate(); - Table table; - List
tables = new ArrayList
(); - boolean only = false; - boolean cascade = false; + Wait wait = new Wait(); + Token token = null; } { -/** -* TRUNCATE can be followed directly by the table name in Postgresql -* See: https://www.postgresql.org/docs/current/sql-truncate.html -* -* TRUNCATE [ TABLE ] [ ONLY ] name [ * ] [, ... ] -* [ RESTART IDENTITY | CONTINUE IDENTITY ] [ CASCADE | RESTRICT ] -* -*/ - - [LOOKAHEAD(2) {truncate.setTableToken(true);}] - [ { only = true; }] - table=Table() { tables.add(table); } (LOOKAHEAD(2) "," table=Table() { tables.add(table); } )* - [ { cascade = true; }] - { - if (only && tables.size() > 1 ) { - throw new ParseException("Cannot TRUNCATE ONLY with multiple tables"); - } else { - return truncate - .withTables(tables) - .withTable(table) - .withOnly(only) - .withCascade(cascade); - } + // sqlserver-oracle-> WAIT (TIMEOUT) + // https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10002.htm#i2126016 + token= + { + wait.setTimeout(Long.parseLong(token.image)); + return wait; } } -/** - * Parses common column-level changes shared between the COLUMN-prefixed and bare forms: - * DROP DEFAULT, SET DEFAULT, SET VISIBLE/INVISIBLE, and bracketed multi-column definitions. - */ -void AlterExpressionColumnChanges(AlterExpression alterExp): +StartTransaction StartTransaction(): { - AlterExpression.ColumnDataType alterExpressionColumnDataType = null; - AlterExpression.ColumnDropDefault alterExpressionColumnDropDefault = null; - AlterExpression.ColumnSetDefault alterExpressionColumnSetDefault = null; - AlterExpression.ColumnSetVisibility alterExpressionColumnSetVisibility = null; + StartTransaction statement = new StartTransaction(); + TransactionKeyword keyword; + StartTransaction.Mode mode; + boolean comma; } { ( - LOOKAHEAD(3) alterExpressionColumnDropDefault = AlterExpressionColumnDropDefault() - { alterExp.addColDropDefault(alterExpressionColumnDropDefault); } - | - LOOKAHEAD(3) alterExpressionColumnSetDefault = AlterExpressionColumnSetDefault() - { alterExp.addColSetDefault(alterExpressionColumnSetDefault); } + ContextualKeyword("TRANSACTION") | - LOOKAHEAD(3) alterExpressionColumnSetVisibility = AlterExpressionColumnSetVisibility() - { alterExp.addColSetVisibility(alterExpressionColumnSetVisibility); } - | - LOOKAHEAD(4) ( - "(" - { alterExp.useBrackets(true);} - alterExpressionColumnDataType = AlterExpressionColumnDataType() { - alterExp.addColDataType(alterExpressionColumnDataType); - } - ( - "," - alterExpressionColumnDataType = AlterExpressionColumnDataType() { - alterExp.addColDataType(alterExpressionColumnDataType); - } - )* - ")" - ) + { statement.setCommand(StartTransaction.Command.BEGIN); } + [ LOOKAHEAD({ getToken(1).kind == K_WORK || isKeywordAhead("TRANSACTION") }) + keyword=TransactionKeyword() { statement.setKeyword(keyword); } ] ) -} - - -AlterExpression.ColumnDataType AlterExpressionColumnDataType(): -{ - String columnName = null; - boolean withType = false; - ColDataType dataType = null; - List columnSpecs = null; - List parameter = null; -} -{ - columnName = RelObjectName() { columnSpecs = new ArrayList(); } - ( LOOKAHEAD(2) { withType = true; } )? - ( LOOKAHEAD(2) dataType = ColDataType() )? - ( LOOKAHEAD(2) parameter = CreateParameter() { columnSpecs.addAll(parameter); } )* + [ + LOOKAHEAD({ isTransactionModeAhead() }) + mode=TransactionMode() { statement.addMode(mode); } + ( + LOOKAHEAD({ ",".equals(getToken(1).image) || isTransactionModeAhead() }) + { comma = false; } + [ "," { comma = true; } ] + mode=TransactionMode() + { requireDdlSyntax(!isMySqlDialect() || comma, "MySQL transaction characteristics require commas"); + statement.addMode(mode); } + )* + ] { - return new AlterExpression.ColumnDataType(columnName, withType, dataType, columnSpecs); + requireDdlSyntax(!isMySqlDialect() || statement.getCommand() != StartTransaction.Command.BEGIN + || statement.getModes().isEmpty(), "MySQL BEGIN does not accept transaction characteristics"); + requireDdlSyntax(!Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) + || !statement.getModes().contains(StartTransaction.Mode.READ_ONLY) + || !statement.getModes().contains(StartTransaction.Mode.READ_WRITE), + "MySQL transactions cannot specify both READ ONLY and READ WRITE"); + return statement; } } -AlterExpression.ColumnDropNotNull AlterExpressionColumnDropNotNull(): +TransactionKeyword TransactionKeyword(): +{ TransactionKeyword keyword; } { - String columnName = null; - boolean withNot = false; - ColDataType dataType = null; - List columnSpecs = null; - List parameter = null; + ( { keyword = TransactionKeyword.WORK; } + | ContextualKeyword("TRANSACTION") { keyword = TransactionKeyword.TRANSACTION; + requireDdlSyntax(!isMySqlDialect(), "MySQL does not accept the TRANSACTION keyword here"); } ) + { return keyword; } } + +TransactionChain TransactionChain(): +{ boolean negative = false; } { - columnName = RelObjectName() - - ( { withNot = true; } )? - - { - return new AlterExpression.ColumnDropNotNull(columnName, withNot); - } + [ { negative = true; } ] ContextualKeyword("CHAIN") + { return negative ? TransactionChain.NO_CHAIN : TransactionChain.CHAIN; } } -AlterExpression.ColumnDropDefault AlterExpressionColumnDropDefault(): +StartTransaction.Mode TransactionMode(): +{ StartTransaction.Mode mode; } { - String columnName = null; - boolean withNot = false; - ColDataType dataType = null; - List columnSpecs = null; - List parameter = null; + ( + LOOKAHEAD({ isKeywordAhead("ISOLATION") }) + ContextualKeyword("ISOLATION") + ( + LOOKAHEAD({ isKeywordAhead("SERIALIZABLE") }) ContextualKeyword("SERIALIZABLE") + { mode = StartTransaction.Mode.ISOLATION_LEVEL_SERIALIZABLE; } + | { mode = StartTransaction.Mode.ISOLATION_LEVEL_REPEATABLE_READ; } + | + ( LOOKAHEAD({ isKeywordAhead("COMMITTED") }) ContextualKeyword("COMMITTED") + { mode = StartTransaction.Mode.ISOLATION_LEVEL_READ_COMMITTED; } + | ContextualKeyword("UNCOMMITTED") { mode = StartTransaction.Mode.ISOLATION_LEVEL_READ_UNCOMMITTED; } ) + ) + { requireDdlSyntax(!isMySqlDialect(), "MySQL START TRANSACTION does not accept ISOLATION LEVEL"); } + | + + ( { mode = StartTransaction.Mode.READ_ONLY; } + | ContextualKeyword("WRITE") { mode = StartTransaction.Mode.READ_WRITE; } ) + | + { mode = StartTransaction.Mode.DEFERRABLE; + requireDdlSyntax(!isMySqlDialect(), "MySQL does not accept DEFERRABLE transactions"); } + | + { mode = StartTransaction.Mode.NOT_DEFERRABLE; + requireDdlSyntax(!isMySqlDialect(), "MySQL does not accept DEFERRABLE transactions"); } + | + ContextualKeyword("CONSISTENT") ContextualKeyword("SNAPSHOT") + { mode = StartTransaction.Mode.WITH_CONSISTENT_SNAPSHOT; + requireDdlSyntax(!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)), + "PostgreSQL does not accept WITH CONSISTENT SNAPSHOT"); } + ) + { return mode; } } + +ReleaseSavepointStatement ReleaseSavepointStatement(): +{ String name; boolean usingSavepointKeyword = false; } { - columnName = RelObjectName() - { - return new AlterExpression.ColumnDropDefault(columnName); - } + + [ LOOKAHEAD({ getToken(1).kind == K_SAVEPOINT }) { usingSavepointKeyword = true; } ] + name=RelObjectName() + { requireDdlSyntax(!isMySqlDialect() || usingSavepointKeyword, "MySQL RELEASE requires SAVEPOINT"); + return new ReleaseSavepointStatement(name).withUsingSavepointKeyword(usingSavepointKeyword); } } -AlterExpression.ColumnSetDefault AlterExpressionColumnSetDefault(): +SavepointStatement SavepointStatement(): +{ String name; } { - String columnName = null; - Expression defaultValue = null; + name=RelObjectName() + { return new SavepointStatement(name); } } + +RollbackStatement RollbackStatement(): { - columnName = RelObjectName() defaultValue = Expression() - { - return new AlterExpression.ColumnSetDefault(columnName, defaultValue.toString()); - } + RollbackStatement statement = new RollbackStatement(); + TransactionKeyword keyword; + TransactionChain chain; + String name; + Token identifier; } - -AlterExpression.ColumnSetVisibility AlterExpressionColumnSetVisibility(): { - String columnName = null; - boolean visible = true; + + [ LOOKAHEAD({ getToken(1).kind == K_WORK || isKeywordAhead("TRANSACTION") }) + keyword=TransactionKeyword() { statement.setKeyword(keyword); } ] + // FORCE INSTALL starts a new statement, whereas FORCE 'id' belongs here. + [ + LOOKAHEAD(2) + ( + [ LOOKAHEAD({ getToken(1).kind == K_SAVEPOINT }) { statement.setUsingSavepointKeyword(true); } ] + name=RelObjectName() { statement.setSavepointName(name); } + | + identifier= { statement.setForceDistributedTransactionIdentifier(identifier.image); } + | + chain=TransactionChain() { statement.setChain(chain); } + ) + ] + { return statement; } } + +Commit Commit(): +{ Commit commit = new Commit(); TransactionKeyword keyword; TransactionChain chain; } { - columnName = RelObjectName() - ( - { visible = true; } | - { visible = false; } - ) - { - return new AlterExpression.ColumnSetVisibility(columnName, visible); - } + + [ LOOKAHEAD({ getToken(1).kind == K_WORK || isKeywordAhead("TRANSACTION") }) + keyword=TransactionKeyword() { commit.setKeyword(keyword); } ] + [ chain=TransactionChain() { commit.setChain(chain); } ] + { return commit; } } -List AlterExpressionConstraintState(): +Comment Comment(): { - List retval = new ArrayList(); + CommentTarget target; + Comment result = new Comment(); + Table table; + Table view; + Column column; + StringValue comment; } { + ( ( - {retval.add(new DeferrableConstraint(false));} - ) - | - ( - {retval.add(new DeferrableConstraint(true));} - ) - | - ( - {retval.add(new ValidateConstraint(false));} + table = Table() { result.setTable(table); } ) | ( - {retval.add(new ValidateConstraint(true));} + column = Column() { result.setColumn(column); } ) | ( - {retval.add(new EnableConstraint(false));} + view = Table() { result.setView(view); } ) | - ( - {retval.add(new EnableConstraint(true));} - ) - )* - { - return retval; - } -} - -Index IndexWithComment(Index index): -{ - Token tk = null; -} -{ - tk= { - index.setCommentText(tk.image); - } - { - return index; - } -} - -void IndexOptionList(List list) : -{} -{ + target=CommentTarget() { result.setTarget(target); } + ) + ( - LOOKAHEAD(2) IndexOption(list) - )* -} - -String UsingIndexType() : -{ - String sk = null; -} -{ - ( sk = RelObjectName() ) + comment=CharacterLiteral(true) { result.setComment(comment); } + | + ) { - return sk; + return result; } } -void IndexOption(List list) : -{ - Token tk1 = null; - Token tk2 = null; - String sk1 = null; - boolean useEqual = false; -} -{ - ( - tk1= ["=" { useEqual = true; } ] tk2= - { - list.add("KEY_BLOCK_SIZE" + (useEqual ? " = " : "") + tk2.image); - } - | - tk1= tk2= - { - list.add("WITH PARSER " + tk2.image); - } - | - tk1= tk2= - { - list.add("COMMENT " + tk2.image); - } - | - tk1= - { - list.add("VISIBLE"); - } - | - tk1= - { - list.add("INVISIBLE"); - } - | - sk1 = UsingIndexType(){ - list.add("USING " + sk1); - } - ) -} - -List PartitionDefinitions(): -{ - Token tk; - List partitionDefinitions = new ArrayList(); - PartitionDefinition partitionDef = null; - String partitionName = null; - String partitionOperation = null; - String storageEngine = null; - Expression exp = null; -} +/** Catalog names are already tokenized into parts; quoted dots are part of one identifier. */ +Table CommentObjectName(): +{ ObjectNames names; } { - "(" - ( - - partitionName=RelObjectName() - { - List values = new ArrayList(); - } - - ( - - ( - "(" exp = Expression() ")"{ - values.add(exp.toString()); - } - | { values.add("MAXVALUE"); } - ) { - partitionOperation = "VALUES LESS THAN"; - } - ) - [ "ENGINE" "=" tk= { storageEngine = tk.image; } ] - { - partitionDef = new PartitionDefinition(partitionName, partitionOperation, values, storageEngine); - partitionDefinitions.add(partitionDef); - } - [ "," ] - )* - ")" - { - return partitionDefinitions; - } + names=RelObjectNames() + { return names.getNames().size() == 1 ? new Table(names.getNames().get(0), false) + : new Table(names.getNames()); } } -List PartitionNamesList() : +/** The object kind decides the shape of the reference that follows it. */ +CommentTarget CommentTarget(): { - Token tk; - List partitionNames = new ArrayList(); + CommentTarget result = new CommentTarget(); + CommentTarget.Kind kind; + Table name; + String objectName; + RoutineReference routine; + ColDataType type; + ExclusionOperator operator; + Long oid; } { + kind=CommentTargetKind(result) { result.setKind(kind); } ( - tk = { - partitionNames.add(tk.image); - } + LOOKAHEAD({ kind.isRoutine() }) + routine=RoutineReference(kind == CommentTarget.Kind.AGGREGATE) { result.setRoutine(routine); } | - tk = { - partitionNames.add(tk.image); - } - ( - LOOKAHEAD(2) "," tk = { - partitionNames.add(tk.image); - } - )* + LOOKAHEAD({ kind.isOwnedByRelation() }) + objectName=RelObjectName() { result.setName(new Table(objectName, false)); } + + [ LOOKAHEAD({ kind == CommentTarget.Kind.CONSTRAINT && getToken(1).kind == K_DOMAIN + && !".".equals(getToken(2).image) && getToken(2).kind != K_IS }) + { result.setOnDomain(true); } ] + name=CommentObjectName() { result.setRelation(name); } + | + LOOKAHEAD({ kind == CommentTarget.Kind.CAST }) + "(" type=ColDataType() { result.setSourceType(type); } + type=ColDataType() { result.setTargetType(type); } ")" + | + LOOKAHEAD({ kind == CommentTarget.Kind.OPERATOR }) + operator=PostgreSqlOperatorReference() { result.setOperator(operator); } + "(" type=ExtensionOperatorType() { result.setLeftOperandType(type); } + "," type=ExtensionOperatorType() { result.setRightOperandType(type); } ")" + { requireTypeDdlSyntax(result.getLeftOperandType() != null || result.getRightOperandType() != null, + "An operator signature needs an operand type"); } + | + LOOKAHEAD({ kind.isOwnedByAccessMethod() }) + name=CommentObjectName() { result.setName(name); } + objectName=RelObjectName() { result.setAccessMethod(objectName); } + | + LOOKAHEAD({ kind == CommentTarget.Kind.LARGE_OBJECT }) + oid=ObjectIdentifier() { result.setLargeObjectOid(oid); } + | + LOOKAHEAD({ kind == CommentTarget.Kind.TRANSFORM }) + type=ColDataType() { result.setTransformType(type); } + ContextualKeyword("LANGUAGE") objectName=RelObjectName() { result.setLanguage(objectName); } + | + name=CommentObjectName() { result.setName(name); } + ) + { return result; } +} + +/** Kind words without a token are matched contextually and remain ordinary identifiers. */ +CommentTarget.Kind CommentTargetKind(CommentTarget target): +{ CommentTarget.Kind kind; } +{ + ( + { kind = CommentTarget.Kind.INDEX; } + | { kind = CommentTarget.Kind.SCHEMA; } + | { kind = CommentTarget.Kind.SEQUENCE; } + | { kind = CommentTarget.Kind.MATERIALIZED_VIEW; } + | { kind = CommentTarget.Kind.DOMAIN; } + | { kind = CommentTarget.Kind.TYPE; } + | { kind = CommentTarget.Kind.FUNCTION; } + | { kind = CommentTarget.Kind.CONSTRAINT; } + | { kind = CommentTarget.Kind.TRIGGER; } + | { kind = CommentTarget.Kind.POLICY; } + | LOOKAHEAD({ isKeywordAhead("RULE") }) ContextualKeyword("RULE") { kind = CommentTarget.Kind.RULE; } + | { kind = CommentTarget.Kind.PROCEDURE; } + | LOOKAHEAD({ isKeywordAhead("ROUTINE") }) ContextualKeyword("ROUTINE") { kind = CommentTarget.Kind.ROUTINE; } + | { kind = CommentTarget.Kind.AGGREGATE; } + | { kind = CommentTarget.Kind.CAST; } + | LOOKAHEAD({ isKeywordAhead("ACCESS") }) ContextualKeyword("ACCESS") ContextualKeyword("METHOD") + { kind = CommentTarget.Kind.ACCESS_METHOD; } + | LOOKAHEAD({ isKeywordAhead("COLLATION") }) ContextualKeyword("COLLATION") { kind = CommentTarget.Kind.COLLATION; } + | { kind = CommentTarget.Kind.DATABASE; } + | { kind = CommentTarget.Kind.EVENT_TRIGGER; } + | LOOKAHEAD({ isKeywordAhead("EXTENSION") }) ContextualKeyword("EXTENSION") { kind = CommentTarget.Kind.EXTENSION; } + | + ( + ContextualKeyword("WRAPPER") { kind = CommentTarget.Kind.FOREIGN_DATA_WRAPPER; } + | { kind = CommentTarget.Kind.FOREIGN_TABLE; } + ) + | LOOKAHEAD({ isKeywordAhead("PROCEDURAL") || isKeywordAhead("LANGUAGE") }) + [ LOOKAHEAD({ isKeywordAhead("PROCEDURAL") }) ContextualKeyword("PROCEDURAL") { target.setProcedural(true); } ] + ContextualKeyword("LANGUAGE") { kind = CommentTarget.Kind.LANGUAGE; } + | LOOKAHEAD({ isKeywordAhead("PUBLICATION") }) ContextualKeyword("PUBLICATION") { kind = CommentTarget.Kind.PUBLICATION; } + | LOOKAHEAD({ isKeywordAhead("ROLE") }) ContextualKeyword("ROLE") { kind = CommentTarget.Kind.ROLE; } + | LOOKAHEAD({ isKeywordAhead("SERVER") }) ContextualKeyword("SERVER") { kind = CommentTarget.Kind.SERVER; } + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) ContextualKeyword("STATISTICS") { kind = CommentTarget.Kind.STATISTICS; } + | LOOKAHEAD({ isKeywordAhead("SUBSCRIPTION") }) ContextualKeyword("SUBSCRIPTION") { kind = CommentTarget.Kind.SUBSCRIPTION; } + | { kind = CommentTarget.Kind.TABLESPACE; } + | LOOKAHEAD({ isKeywordAhead("OPERATOR") }) ContextualKeyword("OPERATOR") { kind = CommentTarget.Kind.OPERATOR; } + // a schema named CLASS or FAMILY may qualify an operator, as in class.+ + [ LOOKAHEAD({ (isKeywordAhead("CLASS") || isKeywordAhead("FAMILY")) && !".".equals(getToken(2).image) }) + ( LOOKAHEAD({ isKeywordAhead("CLASS") }) ContextualKeyword("CLASS") { kind = CommentTarget.Kind.OPERATOR_CLASS; } + | ContextualKeyword("FAMILY") { kind = CommentTarget.Kind.OPERATOR_FAMILY; } ) ] + | LOOKAHEAD({ isKeywordAhead("LARGE") }) ContextualKeyword("LARGE") ContextualKeyword("OBJECT") + { kind = CommentTarget.Kind.LARGE_OBJECT; } + | LOOKAHEAD({ isKeywordAhead("CONVERSION") }) ContextualKeyword("CONVERSION") { kind = CommentTarget.Kind.CONVERSION; } + | LOOKAHEAD({ isKeywordAhead("TRANSFORM") }) ContextualKeyword("TRANSFORM") { kind = CommentTarget.Kind.TRANSFORM; } + | LOOKAHEAD({ isKeywordAhead("TEXT") }) + ( + { kind = CommentTarget.Kind.TEXT_SEARCH_PARSER; } + | LOOKAHEAD({ isKeywordAhead("CONFIGURATION") }) ContextualKeyword("CONFIGURATION") + { kind = CommentTarget.Kind.TEXT_SEARCH_CONFIGURATION; } + | LOOKAHEAD({ isKeywordAhead("DICTIONARY") }) ContextualKeyword("DICTIONARY") + { kind = CommentTarget.Kind.TEXT_SEARCH_DICTIONARY; } + | ContextualKeyword("TEMPLATE") { kind = CommentTarget.Kind.TEXT_SEARCH_TEMPLATE; } + ) ) + { return kind; } +} + +/** An unsigned 32-bit PostgreSQL object identifier, such as the OID of a large object. */ +Long ObjectIdentifier(): +{ Token token; long value; } +{ + token= { - return partitionNames; + try { + value = Long.parseLong(token.image); + } catch (NumberFormatException exception) { + value = -1; + } + requireTypeDdlSyntax(value >= 0 && value <= 0xFFFFFFFFL, "OID out of range: " + token.image); + return value; } } -/** - * Parses DISCARD/IMPORT (PARTITION names TABLESPACE | TABLESPACE). - * Both keywords share the same structure, differing only in operation enum. - */ -AlterExpression AlterExpressionDiscardOrImport(): +/** Consumes a keyword that remains an ordinary identifier outside this grammar context. */ +void ContextualKeyword(String expected): +{ Token keyword; } { - AlterExpression alterExp; - List partitions = null; - AlterOperation partOp; - AlterOperation tableOp; + keyword= { + if (!expected.equalsIgnoreCase(keyword.image)) { + throw new ParseException("Expected " + expected + " but found " + keyword.image); + } + } } + +void AccessKeyword(String expected): +{ Token token; } +{ (token= | token=) { requireAccessSyntax(expected.equalsIgnoreCase(token.image), "Expected " + expected); } } + +String AccessQualifiedName(): +{ String name; String part; } +{ name=RelObjectName() ( "." part=RelObjectNameExt() { name += "." + part; } )* { return name; } } + +List AccessNames(): +{ List names = new ArrayList(); String name; } +{ name=RelObjectName() { names.add(name); } ( "," name=RelObjectName() { names.add(name); } )* { return names; } } + +RoleOption RoleOption(boolean create): +{ RoleOption option = null; Token token; Token value; String sign = ""; Expression expression; List names; boolean encrypted = false; } { ( - { partOp = AlterOperation.DISCARD_PARTITION; tableOp = AlterOperation.DISCARD_TABLESPACE; } - | - { partOp = AlterOperation.IMPORT_PARTITION; tableOp = AlterOperation.IMPORT_TABLESPACE; } - ) - ( - - { alterExp = new AlterExpressionPartition(); alterExp.setOperation(partOp); } - partitions = PartitionNamesList() - { alterExp.setPartitions(partitions); } - - { alterExp.setTableOption("TABLESPACE"); } - | - - { alterExp = new AlterExpressionTableOption(); alterExp.setOperation(tableOp); } + LOOKAHEAD({ isAccessKeywordAhead("CONNECTION") }) + AccessKeyword("CONNECTION") [ ( "+" | "-" { sign = "-"; } ) ] value= + { option = new RoleOption(RoleOption.Kind.CONNECTION_LIMIT); option.setValue(new LongValue(sign + value.image)); } + | + LOOKAHEAD({ isAccessKeywordAhead("PASSWORD") || isAccessKeywordAhead("ENCRYPTED") }) + [ AccessKeyword("ENCRYPTED") { encrypted = true; } ] + ( value= { expression = stringValue(value); } | { expression = new NullValue(); } ) + { option = new RoleOption(RoleOption.Kind.PASSWORD); option.setEncrypted(encrypted); option.setValue(expression); } + | + LOOKAHEAD({ isAccessKeywordAhead("VALID") }) + AccessKeyword("VALID") AccessKeyword("UNTIL") value= + { option = new RoleOption(RoleOption.Kind.VALID_UNTIL); option.setValue(stringValue(value)); } + | + + ( AccessKeyword("ROLE") { option = new RoleOption(RoleOption.Kind.IN_ROLE); } + | { option = new RoleOption(RoleOption.Kind.IN_GROUP); } ) + names=AccessNames() + { requireAccessSyntax(create, "Membership attributes require CREATE ROLE"); option.setRoles(names); } + | + LOOKAHEAD({ isAccessKeywordAhead("ROLE") || isAccessKeywordAhead("USER") || isAccessKeywordAhead("ADMIN") }) + ( token= | token= ) + names=AccessNames() + { requireAccessSyntax(create, "Membership attributes require CREATE ROLE"); option = new RoleOption(accessEnum(RoleOption.Kind.class, token.image)); option.setRoles(names); } + | + LOOKAHEAD({ isAccessKeywordAhead("SYSID") }) + AccessKeyword("SYSID") value= + { requireAccessSyntax(create, "SYSID requires CREATE ROLE"); option = new RoleOption(RoleOption.Kind.SYSID); option.setValue(new LongValue(value.image)); } + | + token= { option = new RoleOption(accessEnum(RoleOption.Kind.class, token.image)); } ) - { return alterExp; } + { return option; } } +CreateRole CreateRole(): +{ CreateRole role = new CreateRole(); String name; RoleOption option; Token token; } +{ + ( token= | token= | token= ) { role.setCommand(accessEnum(CreateRole.Command.class, token.image)); } + name=RelObjectName() { role.setName(name); } + [ LOOKAHEAD(1) { role.setUseWith(true); } ] + ( LOOKAHEAD({ isRoleOptionAhead(1) }) option=RoleOption(true) { role.getOptions().add(option); } )* + { requireAccessSyntax(!role.isUseWith() || !role.getOptions().isEmpty(), "WITH requires role attributes"); return role; } +} -/** - * Parses ADD/ALTER CONSTRAINT clause within AlterExpression. - * Handles: CONSTRAINT [UNIQUE [KEY|INDEX]] name columns - * CONSTRAINT name (FOREIGN KEY|PRIMARY KEY|UNIQUE|KEY|CHECK|[NOT] ENFORCED) - */ -void AlterExpressionAddConstraint(AlterExpression alterExp): +AlterSchema AlterSchema(): { - Token tk; - Token tk2 = null; - String sk3 = null; - List columnNames = null; - ForeignKeyIndex fkIndex = null; - Index index = null; - Table fkTable; - List constraints = null; - CheckConstraint checkCs = null; + AlterSchema statement = new AlterSchema(); + String name; } { - + name=RelObjectName() { statement.setSchemaName(name); } ( - LOOKAHEAD(2) - ( - ( { alterExp.setConstraintType("UNIQUE KEY"); } - | { alterExp.setConstraintType("UNIQUE INDEX"); } - | { alterExp.setConstraintType("UNIQUE"); } ) - sk3=RelObjectName() { - alterExp.setConstraintSymbol(sk3); - index = new Index(); - } - columnNames=ColumnsNamesList() { - index.setColumnsNames(columnNames); - alterExp.setIndex(index); - } - ) + name=RelObjectName() + { statement.setAction(AlterSchema.Action.RENAME); statement.setNewName(name); } | - sk3=RelObjectName() - ( - ( tk= tk2= - columnNames=ColumnsNamesList() - { - fkIndex = new ForeignKeyIndex() - .withName(sk3) - .withType(tk.image + " " + tk2.image) - .withColumnsNames(columnNames); - columnNames = null; - } - fkTable=Table() [ LOOKAHEAD(2) columnNames=ColumnsNamesList() ] - { - fkIndex.withTable(fkTable).withReferencedColumnNames(columnNames); - alterExp.setIndex(fkIndex); - } - ReferentialActionsOnIndex(fkIndex) - constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } - ) - | - ( tk= tk2= - columnNames=ColumnsNamesList() - { - index = new NamedConstraint() - .withName(sk3) - .withType(tk.image + " " + tk2.image) - .withColumnsNames(columnNames); - alterExp.setIndex(index); - } - AlterExpressionConstraintTail(alterExp, index) - ) - | - LOOKAHEAD(2) ( - { boolean enforced = true; } - [ tk = { enforced = false; } ] - { - alterExp.setEnforced(enforced); - alterExp.setConstraintType("CONSTRAINT"); - alterExp.setConstraintSymbol(sk3); - } - ) - | - ( - checkCs = CheckConstraintSpec(sk3) - { alterExp.setIndex(checkCs); } - ) - | - ( - tk= (tk2= { alterExp.setUk(true); } | tk2=)? - columnNames=ColumnsNamesList() - { - index = new NamedConstraint() - .withName(sk3) - .withType(tk.image + (tk2!=null?" " + tk2.image:"")) - .withColumnsNames(columnNames); - alterExp.setIndex(index); - } - AlterExpressionConstraintTail(alterExp, index) - ) - | - ( - tk= - columnNames=ColumnsNamesList() - { - index = new NamedConstraint() - .withName(sk3) - .withType(tk.image) - .withColumnsNames(columnNames); - alterExp.setIndex(index); - } - constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } - ) - ) + ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterSchema.Action.OWNER); statement.setOwner(name); } ) + { return statement; } } -/** - * Parses the DROP operations within AlterExpression. - * Handles: DROP (PARTITION|columns|COLUMN|INDEX|KEY|UNIQUE|PRIMARY KEY|FOREIGN KEY|CONSTRAINT) - */ -AlterExpression AlterExpressionDrop(): +AlterRole AlterRole(): { - AlterExpression alterExp = new AlterExpressionDrop(); - Token tk; - Token tk2; - List columnNames = null; - List partitions = null; - Index index = null; + AlterRole role = new AlterRole(); String name; RoleOption option; Token token; + ConfigurationParameterChange change; List names; } { - { alterExp.setOperation(AlterOperation.DROP); } + ( token= | token= | token= ) { role.setCommand(accessEnum(CreateRole.Command.class, token.image)); } + ( LOOKAHEAD(1) { role.setName("ALL"); } | name=RelObjectName() { role.setName(name); } ) + [ LOOKAHEAD(2) name=RelObjectName() { role.setDatabase(name); } ] ( - ( - { - alterExp.setOperation(AlterOperation.DROP_PARTITION); - } - partitions=PartitionNamesList() { - alterExp.setPartitions(partitions); - } - ) - | - ( - // Oracle Multi Column Drop - columnNames=ColumnsNamesList() { alterExp.setPkColumns(columnNames); columnNames = null; } - [ "INVALIDATE" { alterExp.addParameters("INVALIDATE"); } ] - [ - "CASCADE" { alterExp.addParameters("CASCADE"); } - [ "CONSTRAINTS" { alterExp.addParameters("CONSTRAINTS"); } ] - ] - ) - | - ( - ( LOOKAHEAD(2) { alterExp.hasColumn(true); } )? - [ { alterExp.setUsingIfExists(true); } ] - (tk=KeywordOrIdentifier() ) { alterExp.setColumnName(tk.image); } - [ "INVALIDATE" { alterExp.addParameters("INVALIDATE"); } ] - [ - "CASCADE" { alterExp.addParameters("CASCADE"); } - [ "CONSTRAINTS" { alterExp.addParameters("CONSTRAINTS"); } ] - ] - ) - | - ( - ( tk= | tk= ) - ( tk2= | tk2= ) { - index = new Index().withType(tk.image).withName(tk2.image); - alterExp.setIndex(index); - } - ) + name=RelObjectName() { role.setAction(AlterRole.Action.RENAME); role.setNewName(name); } | - ( - tk= { alterExp.setOperation(AlterOperation.DROP_UNIQUE); } - columnNames=ColumnsNamesList() { alterExp.setPkColumns(columnNames); columnNames = null; } - [ ( tk= | tk= ) { alterExp.addParameters(tk.image); } ] - ) - | - ( - tk= tk2= { alterExp.setOperation(AlterOperation.DROP_PRIMARY_KEY); } - [ ( tk= | tk= ) { alterExp.addParameters(tk.image); } ] - ) + ( { role.setAction(AlterRole.Action.ADD_USER); } | { role.setAction(AlterRole.Action.DROP_USER); } ) + names=AccessNames() { requireAccessSyntax(role.getCommand() == CreateRole.Command.GROUP, "ADD/DROP USER requires ALTER GROUP"); role.setUsers(names); } | - ( - tk= tk2= { alterExp.setOperation(AlterOperation.DROP_FOREIGN_KEY); } - columnNames=ColumnsNamesList() { alterExp.setPkColumns(columnNames); columnNames = null; } - [ ( tk= | tk= ) { alterExp.addParameters(tk.image); } ] - ) + change=ConfigurationParameterChange() { role.setParameterChange(change); } | - ( - [ { alterExp.setUsingIfExists(true); } ] - ( tk= | tk=) { alterExp.setConstraintName(tk.image); } - [ ( tk= | tk= ) { alterExp.addParameters(tk.image); } ] - ) + [ { role.setUseWith(true); } ] + option=RoleOption(false) { role.getOptions().add(option); role.setAction(AlterRole.Action.OPTIONS); } + ( LOOKAHEAD({ isRoleOptionAhead(1) }) option=RoleOption(false) { role.getOptions().add(option); } )* ) - { return alterExp; } + { + requireAccessSyntax(role.getDatabase() == null && !"ALL".equals(role.getName()) + || role.getAction() == AlterRole.Action.SET || role.getAction() == AlterRole.Action.RESET, + "ALL and IN DATABASE require SET or RESET"); + requireAccessSyntax(role.getCommand() != CreateRole.Command.GROUP || role.getAction() == AlterRole.Action.RENAME + || role.getAction() == AlterRole.Action.ADD_USER || role.getAction() == AlterRole.Action.DROP_USER, "Invalid ALTER GROUP action"); + return role; + } } /** - * Parses partition maintenance operations within AlterExpression. - * Handles: TRUNCATE/ANALYZE/CHECK/OPTIMIZE/REBUILD/REPAIR PARTITION, - * COALESCE/REORGANIZE/EXCHANGE/PARTITION BY, REMOVE PARTITIONING + * PostgreSQL SET/RESET of a configuration parameter, shared by ALTER ROLE, ALTER DATABASE and + * ALTER FUNCTION/PROCEDURE/ROUTINE. */ -AlterExpression AlterExpressionPartitionOp(): +ConfigurationParameterChange ConfigurationParameterChange(): { - AlterExpression alterExp = new AlterExpressionPartition(); - Token tk; - List partitions = null; - List partitionDefinition = null; - List columnNames = null; - Expression exp = null; + ConfigurationParameterChange change = new ConfigurationParameterChange(); + String name; + Expression value; + ExpressionList values = new ExpressionList(); } { ( - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.TRUNCATE_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) tk= { - alterExp.setOperation(AlterOperation.COALESCE_PARTITION); - alterExp.setCoalescePartitionNumber(Integer.valueOf(tk.image)); - } - | - LOOKAHEAD(2) - partitions=PartitionNamesList() partitionDefinition=PartitionDefinitions() { - alterExp.setOperation(AlterOperation.REORGANIZE_PARTITION); - alterExp.setPartitions(partitions); - alterExp.setPartitionDefinitions(partitionDefinition); - } - | - LOOKAHEAD(2) partitions=PartitionNamesList() - tk= - [ - LOOKAHEAD(2) ( - { alterExp.setExchangePartitionWithValidation(true); } - | - { alterExp.setExchangePartitionWithoutValidation(false); } - ) - ] - { - alterExp.setOperation(AlterOperation.EXCHANGE_PARTITION); - alterExp.setPartitions(partitions); - alterExp.setExchangePartitionTableName(tk.image); - } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.ANALYZE_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.CHECK_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.OPTIMIZE_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.REBUILD_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.REPAIR_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.REMOVE_PARTITIONING); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.PARTITION_BY); } - { alterExp.setPartitionType("RANGE"); } + name=AccessQualifiedName() { change.setParameter(name); } ( - "(" exp=Expression() ")" { alterExp.setPartitionExpression(exp); } + { change.setValueKind(ConfigurationParameterChange.ValueKind.FROM_CURRENT); } | - columnNames=ColumnsNamesList() { alterExp.setPartitionColumns(columnNames); } + ( | "=" { change.setUseEquals(true); } ) + ( + LOOKAHEAD(1) { change.setValueKind(ConfigurationParameterChange.ValueKind.DEFAULT); } + | + value=AccessSettingValue() { values.add(value); } + ( "," value=AccessSettingValue() { values.add(value); } )* + { change.setValues(values); } + ) ) - partitionDefinition=PartitionDefinitions() { alterExp.setPartitionDefinitions(partitionDefinition); } + | + { change.setAction(ConfigurationParameterChange.Action.RESET); } + ( LOOKAHEAD(1) { change.setParameter("ALL"); } | name=AccessQualifiedName() { change.setParameter(name); } ) ) - { return alterExp; } + { return change; } } -/** - * Parses ADD/ALTER/MODIFY operations within ALTER TABLE. - * Handles: PRIMARY KEY, INDEX/KEY, SPATIAL/FULLTEXT, column COMMENT, - * ADD PARTITION, column definitions, UNIQUE, FOREIGN KEY, CHECK ENFORCED, CONSTRAINT. - */ -AlterExpression AlterExpressionAddAlterModify(): +Expression AccessSettingValue(): +{ Expression value; Token token; } +{ ( LOOKAHEAD(1) token= { value = linkToLastToken(new Column(token.image)); } | value=PrimaryExpression() ) { return value; } } + +/** PostgreSQL and MySQL static privileges; another identifier is a MySQL dynamic privilege. */ +Privilege AccessPrivilege(): +{ Privilege result; Privilege.Kind kind = null; Token token = null; String column; ExpressionList columns = null; } { - AlterExpression alterExp = new AlterExpression(); - Token tk; - Token tk2 = null; - String sk3 = null; - String sk4 = null; - List columnNames = null; - List indexColumnNames = null; - List constraints = null; - Index index = null; - AlterExpression.ColumnDataType alterExpressionColumnDataType = null; - AlterExpression.ColumnDropNotNull alterExpressionColumnDropNotNull = null; - List indexSpec = new ArrayList(); - List partitionDefinition = null; + ( + { kind = Privilege.Kind.ALTER; } + [ { kind = Privilege.Kind.ALTER_SYSTEM; } + | LOOKAHEAD({ isAccessKeywordAhead("ROUTINE") }) AccessKeyword("ROUTINE") { kind = Privilege.Kind.ALTER_ROUTINE; } ] + | + { kind = Privilege.Kind.CREATE; } + [ { kind = Privilege.Kind.CREATE_VIEW; } + | { kind = Privilege.Kind.CREATE_USER; } + | { kind = Privilege.Kind.CREATE_TABLESPACE; } + | { kind = Privilege.Kind.CREATE_TEMPORARY_TABLES; } + | LOOKAHEAD({ isAccessKeywordAhead("ROLE") }) AccessKeyword("ROLE") { kind = Privilege.Kind.CREATE_ROLE; } + | LOOKAHEAD({ isAccessKeywordAhead("ROUTINE") }) AccessKeyword("ROUTINE") { kind = Privilege.Kind.CREATE_ROUTINE; } ] + | + { kind = Privilege.Kind.DROP; } + [ LOOKAHEAD({ isAccessKeywordAhead("ROLE") }) AccessKeyword("ROLE") { kind = Privilege.Kind.DROP_ROLE; } ] + | + { kind = Privilege.Kind.GRANT_OPTION; } + | + { kind = Privilege.Kind.LOCK_TABLES; } + | + ( { kind = Privilege.Kind.SHOW_VIEW; } | AccessKeyword("DATABASES") { kind = Privilege.Kind.SHOW_DATABASES; } ) + | + LOOKAHEAD({ isAccessKeywordAhead("REPLICATION") && (getToken(2).kind == K_SLAVE || "CLIENT".equalsIgnoreCase(getToken(2).image)) }) + AccessKeyword("REPLICATION") + ( { kind = Privilege.Kind.REPLICATION_SLAVE; } | AccessKeyword("CLIENT") { kind = Privilege.Kind.REPLICATION_CLIENT; } ) + | + ( token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= ) + { kind = accessEnum(Privilege.Kind.class, token.image); } + | + token= + { if (Arrays.asList("MAINTAIN", "USAGE", "PROCESS", "PROXY", "RELOAD", "SUPER").contains(token.image.toUpperCase(Locale.ROOT))) { + kind = accessEnum(Privilege.Kind.class, token.image); + requireAccessSyntax(kind != Privilege.Kind.PROXY, "PROXY requires an account target"); + } } + ) + { result = kind == null ? Privilege.dynamic(token.image) : new Privilege(kind); } + [ LOOKAHEAD({ isAccessKeywordAhead("PRIVILEGES") }) + AccessKeyword("PRIVILEGES") { requireAccessSyntax(result.getKind() == Privilege.Kind.ALL, "PRIVILEGES requires ALL"); result.setUsePrivileges(true); } ] + [ "(" { columns = new ExpressionList(); } + column=RelObjectName() { columns.add(linkToLastToken(new Column(column))); } + ( "," column=RelObjectName() { columns.add(linkToLastToken(new Column(column))); } )* + ")" { result.setColumns(linkElements(columns)); } ] + { return result; } } + +PrivilegeTarget.Kind AccessObjectKind(boolean plural): +{ Token token; String name; } { ( - { alterExp.setOperation(AlterOperation.ADD); } + + ( AccessKeyword("WRAPPER") { name = "FOREIGN_DATA_WRAPPER"; } + | AccessKeyword("SERVER") { name = "FOREIGN_SERVER"; } ) | - { alterExp.setOperation(AlterOperation.ALTER); } + LOOKAHEAD({ isAccessKeywordAhead("LARGE") }) + AccessKeyword("LARGE") ( token= ) { name = "LARGE_" + token.image.toUpperCase(Locale.ROOT); } | - { alterExp.setOperation(AlterOperation.MODIFY); } + ( token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= ) + { name = token.image.toUpperCase(Locale.ROOT); } ) + { + if (plural) { + requireAccessSyntax(name.endsWith("S"), "Expected plural privilege target"); + name = name.substring(0, name.length() - 1); + } + return accessEnum(PrivilegeTarget.Kind.class, name); + } +} - ( - LOOKAHEAD(2) ( columnNames=ColumnsNamesList() { alterExp.setPkColumns(columnNames); }) +RoutineReference.Argument AccessRoutineArgument(): +{ RoutineReference.Argument argument; } +{ argument=RoutineArgument(false) { return argument; } } - constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } - [ - AlterExpressionUsingIndex(alterExp) - ] - | - LOOKAHEAD(2) ( - (tk= { alterExp.setUk(true); } | tk=) - ( - LOOKAHEAD(3) - sk3 = RelObjectName() - [ LOOKAHEAD(2) sk4 = UsingIndexType() ] - [ LOOKAHEAD(2) indexColumnNames = IndexColumnsWithParamsList() ] - | - [ LOOKAHEAD(2) sk4 = UsingIndexType() ] - [ LOOKAHEAD(2) indexColumnNames = IndexColumnsWithParamsList() ] - ) - IndexOptionList(indexSpec = new ArrayList()) - { - index = new Index() - .withIndexKeyword(tk.image) - .withName(sk3) - .withUsing(sk4) - .withColumns(indexColumnNames) - .withIndexSpec(indexSpec); +RoutineReference AccessRoutine(): +{ RoutineReference result = new RoutineReference(); String name; RoutineReference.Argument argument; List arguments; } +{ + name=AccessQualifiedName() { result.setName(name); } + [ LOOKAHEAD(1) "(" { arguments = new ArrayList(); result.setArguments(arguments); } + [ argument=AccessRoutineArgument() { arguments.add(argument); } + ( "," argument=AccessRoutineArgument() { arguments.add(argument); } )* ] ")" ] + { return result; } +} - alterExp.setIndex(index); - } - constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } - ) - | - LOOKAHEAD(4) +PrivilegeTarget AccessTarget(boolean defaults): +{ PrivilegeTarget target = new PrivilegeTarget(); PrivilegeTarget.Kind kind; ObjectNames name; RoutineReference routine; Token number; } +{ + ( + LOOKAHEAD({ defaults }) + kind=AccessObjectKind(true) + { requireAccessSyntax(defaults && EnumSet.of(PrivilegeTarget.Kind.TABLE, PrivilegeTarget.Kind.SEQUENCE, PrivilegeTarget.Kind.FUNCTION, + PrivilegeTarget.Kind.ROUTINE, PrivilegeTarget.Kind.TYPE, PrivilegeTarget.Kind.SCHEMA, PrivilegeTarget.Kind.LARGE_OBJECT).contains(kind), + "Invalid default-privilege target"); + target.setKind(kind); target.setDefaultPrivileges(true); } + | + LOOKAHEAD(1) kind=AccessObjectKind(true) + { requireAccessSyntax(!defaults && EnumSet.of(PrivilegeTarget.Kind.TABLE, PrivilegeTarget.Kind.SEQUENCE, + PrivilegeTarget.Kind.FUNCTION, PrivilegeTarget.Kind.PROCEDURE, PrivilegeTarget.Kind.ROUTINE).contains(kind), + "Invalid ALL IN SCHEMA target"); target.setKind(kind); target.setAllInSchema(true); } + name=RelObjectNames() { target.getNames().add(name.getNames()); } + ( "," name=RelObjectNames() { target.getNames().add(name.getNames()); } )* + | + LOOKAHEAD({ isAccessTargetKindAhead() && (isAccessKeywordAhead("FUNCTION") || isAccessKeywordAhead("PROCEDURE") || isAccessKeywordAhead("ROUTINE")) }) + kind=AccessObjectKind(false) { target.setKind(kind); target.setExplicitKind(true); } + routine=AccessRoutine() { target.getRoutines().add(routine); } + ( "," routine=AccessRoutine() { target.getRoutines().add(routine); } )* + | + LOOKAHEAD({ isAccessTargetKindAhead() && isAccessKeywordAhead("LARGE") }) + kind=AccessObjectKind(false) { target.setKind(kind); target.setExplicitKind(true); } + number= { target.getNames().add(Collections.singletonList(number.image)); } + ( "," number= { target.getNames().add(Collections.singletonList(number.image)); } )* + | + [ LOOKAHEAD({ isAccessTargetKindAhead() }) kind=AccessObjectKind(false) { target.setKind(kind); target.setExplicitKind(true); } ] ( - ( tk= | tk= ) - [ LOOKAHEAD(2) ( tk2= | tk2= ) ] - ( - sk3 = RelObjectName() - columnNames = ColumnsNamesList() - | - columnNames = ColumnsNamesList() - ) - IndexOptionList(indexSpec = new ArrayList()) - { - String type = tk.image; - String keyword = tk2 != null ? tk2.image : null; - index = new Index() - .withType(type) - .withIndexKeyword(keyword) - .withColumnsNames(columnNames) - .withIndexSpec(indexSpec); - - if (sk3 != null) { - index.setName(sk3); - } - - alterExp.setIndex(index); - } - ) - | - LOOKAHEAD(2) ( - sk3=RelObjectName() tk= { alterExp.withColumnName(sk3).withCommentText(tk.image); } - ) - | - LOOKAHEAD(3) ( - { - alterExp.setOperation(AlterOperation.ADD_PARTITION); - } - partitionDefinition=PartitionDefinitions() { - alterExp.setPartitionDefinitions(partitionDefinition); - } - ) - | - LOOKAHEAD(3) ( - ( LOOKAHEAD(2) - ( - { alterExp.hasColumn(true); } - | - { alterExp.hasColumns(true); } - ) - )? - [ LOOKAHEAD(2) { alterExp.setUseIfNotExists(true); } ] + // MySQL privilege levels *, *.* and db.* + "*" { target.setWildcard(PrivilegeTarget.Wildcard.DEFAULT_DATABASE); } + [ "." "*" { target.setWildcard(PrivilegeTarget.Wildcard.GLOBAL); } ] + | + name=RelObjectNames() { target.getNames().add(name.getNames()); } ( - LOOKAHEAD(3) AlterExpressionColumnChanges(alterExp) - | - LOOKAHEAD(2) alterExpressionColumnDataType = AlterExpressionColumnDataType() - { alterExp.addColDataType(alterExpressionColumnDataType); } + "." "*" + { requireAccessSyntax(name.getNames().size() == 1, "Expected database.*"); target.setWildcard(PrivilegeTarget.Wildcard.DATABASE); } | - LOOKAHEAD(3) alterExpressionColumnDropNotNull = AlterExpressionColumnDropNotNull() - { alterExp.addColDropNotNull( alterExpressionColumnDropNotNull);} + ( "," name=RelObjectNames() { target.getNames().add(name.getNames()); } )* ) ) + { requireAccessSyntax(target.getWildcard() == null || target.getKind() == PrivilegeTarget.Kind.TABLE, "Wildcards require a table target"); } + ) + { requireAccessSyntax(target.isDefaultPrivileges() == defaults, "Invalid default-privilege target"); return target; } +} + +/** A role specification shared by MySQL and PostgreSQL grantees, grantors and accounts. */ +Grantee AccessGrantee(): +{ Grantee grantee; StringValue name; UserAccount account; Token token; } +{ + ( + LOOKAHEAD({ getToken(1).kind == K_GROUP }) name=MySqlAccountNamePart() + { grantee = new Grantee(Grantee.Kind.GROUP).withAccount(new UserAccount().withUser(name)); } | - LOOKAHEAD(3) AlterExpressionColumnChanges(alterExp) + LOOKAHEAD({ isAccessSpecialGranteeAhead() }) ( token= | token= ) + { grantee = new Grantee(accessEnum(Grantee.Kind.class, token.image)); } + [ LOOKAHEAD(2) "(" ")" { requireAccessSyntax(grantee.getKind() == Grantee.Kind.CURRENT_USER, "Only CURRENT_USER takes parentheses"); + grantee.setCurrentUserParentheses(true); } ] | - ( - - ( - ( - { alterExp.setUk(true); } - | { alterExp.setUk(false); } - ) - [ (tk= | tk=) { alterExp.setUkName(tk.image); } ] - | - (tk= | tk=) { - alterExp.setUkTypeSpecified(false); - alterExp.setUkName(tk.image); - } - )? - columnNames=ColumnsNamesList() { alterExp.setUkColumns(columnNames); } - [ - AlterExpressionUsingIndex(alterExp) - ] - [ LOOKAHEAD(2) index = IndexWithComment(index) { alterExp.setIndex(index); } ] - ) + account=MySqlAccountName() { grantee = new Grantee(account); } + ) + { return grantee; } +} + +List AccessGrantees(): +{ List grantees = new ArrayList(); Grantee grantee; } +{ grantee=AccessGrantee() { grantees.add(grantee); } ( "," grantee=AccessGrantee() { grantees.add(grantee); } )* { return grantees; } } + +PrivilegeClause AccessClause(boolean revoke, boolean defaults): +{ PrivilegeClause clause = new PrivilegeClause(); Privilege privilege; PrivilegeTarget target; List roles; List grantees; Grantee account; } +{ + ( + LOOKAHEAD({ isAccessKeywordAhead("PROXY") && getToken(2).kind == K_ON }) + AccessKeyword("PROXY") account=AccessGrantee() + { + requireAccessSyntax(!defaults, "PROXY is not part of default privileges"); + requireAccessSyntax(account.getKind() == Grantee.Kind.ACCOUNT || account.getKind() == Grantee.Kind.CURRENT_USER, "PROXY requires an account"); + clause.setPrivileges(new ArrayList()); + clause.getPrivileges().add(new Privilege(Privilege.Kind.PROXY)); + target = new PrivilegeTarget(); target.setKind(PrivilegeTarget.Kind.ACCOUNT); target.setAccount(account); + clause.setTarget(target); + } | - // Standalone FK now uses ForeignKeyIndex, same as CONSTRAINT FK - ( - { ForeignKeyIndex fkIndex; ReferentialAction ra; } - fkIndex = ForeignKeySpec(null) - { - alterExp.setIndex(fkIndex); - // backward compat: populate deprecated FK fields from ForeignKeyIndex - alterExp.setFkColumns(fkIndex.getColumnsNames()); - if (fkIndex.getTable() != null) { - alterExp.setFkSourceSchema(fkIndex.getTable().getSchemaName()); - alterExp.setFkSourceTable(fkIndex.getTable().getName()); - } - alterExp.setFkSourceColumns(fkIndex.getReferencedColumnNames()); - ra = fkIndex.getReferentialAction(ReferentialAction.Type.DELETE); - if (ra != null) { alterExp.setReferentialAction(ra.getType(), ra.getAction()); } - ra = fkIndex.getReferentialAction(ReferentialAction.Type.UPDATE); - if (ra != null) { alterExp.setReferentialAction(ra.getType(), ra.getAction()); } - } - ) + LOOKAHEAD({ isAccessAllGrantOptionAhead() }) + { privilege = new Privilege(Privilege.Kind.ALL); } + [ AccessKeyword("PRIVILEGES") { privilege.setUsePrivileges(true); } ] + "," + { + requireAccessSyntax(!defaults, "ALL, GRANT OPTION is not part of default privileges"); + clause.setPrivileges(new ArrayList()); + clause.getPrivileges().add(privilege); + clause.getPrivileges().add(new Privilege(Privilege.Kind.GRANT_OPTION)); + clause.setTarget(null); + } | - LOOKAHEAD(3) ( - sk3=RelObjectName() - { boolean enforced = true; } - [ tk = { enforced = false; } ] - { - alterExp.setEnforced(enforced); - alterExp.setConstraintType("CHECK"); - alterExp.setConstraintSymbol(sk3); - } - ) + LOOKAHEAD({ isObjectPrivilegeAhead() }) + { clause.setPrivileges(new ArrayList()); } + privilege=AccessPrivilege() { clause.getPrivileges().add(privilege); } + ( "," privilege=AccessPrivilege() { clause.getPrivileges().add(privilege); } )* + target=AccessTarget(defaults) { clause.setTarget(target); } | - ( - AlterExpressionAddConstraint(alterExp) - ) + roles=MySqlAccountNameList() { clause.setRoleItems(requireRoleNames(roles)); clause.setTarget(null); } ) - { return alterExp; } + ( { requireAccessSyntax(!revoke, "REVOKE requires FROM"); } | { requireAccessSyntax(revoke, "GRANT requires TO"); } ) + grantees=AccessGrantees() { clause.setGranteeItems(grantees); return clause; } +} + +GrantOption AccessGrantOption(): +{ GrantOption option; Token token; } +{ + ( token= | token= | token= ) { option = new GrantOption(accessEnum(GrantOption.Kind.class, token.image)); } + ( token= | token= | token= ) { option.setValue(accessEnum(GrantOption.Value.class, token.image)); return option; } +} + +Grantee AccessGrantor(boolean defaults): +{ Grantee grantor; } +{ + AccessKeyword("GRANTED") grantor=AccessGrantee() + { + requireAccessSyntax(!defaults, "GRANTED BY is not part of default privileges"); + requireAccessSyntax(grantor.getKind() != Grantee.Kind.PUBLIC && grantor.getKind() != Grantee.Kind.GROUP, "Invalid grantor"); + return grantor; + } +} + +Grant Grant(): +{ Grant grant; } +{ grant=AccessGrant(false) { return grant; } } + +Grant AccessGrant(boolean defaults): +{ Grant grant = new Grant(); PrivilegeClause clause; GrantOption option; Grantee grantee; RoleSelection roles; } +{ + clause=AccessClause(false, defaults) { grant.setClause(clause); } + [ LOOKAHEAD(1) + option=AccessGrantOption() { grant.getOptions().add(option); } + ( "," option=AccessGrantOption() { grant.getOptions().add(option); } )* + { requireAccessSyntax(isValidAccessGrantOptions(clause, grant.getOptions()), "Invalid GRANT option"); } + ] + [ grantee=AccessGrantee() + { + requireAccessSyntax(!defaults && clause.getPrivileges() != null && clause.getTarget() != null + && clause.getTarget().getKind() != PrivilegeTarget.Kind.ACCOUNT, "AS requires object privileges"); + requireAccessSyntax(grantee.getKind() == Grantee.Kind.ACCOUNT || grantee.getKind() == Grantee.Kind.CURRENT_USER, "AS requires an account"); + grant.setAsUser(grantee); + } + [ LOOKAHEAD({ getToken(1).kind == K_WITH && "ROLE".equalsIgnoreCase(getToken(2).image) }) + AccessKeyword("ROLE") roles=MySqlRoleSelection(true) { grant.setWithRole(roles); } ] + ] + [ LOOKAHEAD({ isAccessKeywordAhead("GRANTED") }) grantee=AccessGrantor(defaults) { clause.setGrantor(grantee); } ] + { return grant; } +} + +Revoke Revoke(boolean defaults): +{ Revoke revoke = new Revoke(); PrivilegeClause clause; Token token; Grantee grantor; } +{ + + [ LOOKAHEAD(2) + { requireAccessSyntax(!defaults, "IF EXISTS is not part of default privileges"); revoke.setIfExists(true); } ] + [ LOOKAHEAD(3) ( token= | token= | token= ) + { revoke.setOptionFor(accessEnum(GrantOption.Kind.class, token.image)); } ] + clause=AccessClause(true, defaults) { revoke.setClause(clause); } + [ LOOKAHEAD({ isAccessKeywordAhead("GRANTED") }) grantor=AccessGrantor(defaults) { clause.setGrantor(grantor); } ] + [ ( { revoke.setBehavior(CascadeBehavior.CASCADE); } | { revoke.setBehavior(CascadeBehavior.RESTRICT); } ) ] + [ + { requireAccessSyntax(!defaults, "IGNORE UNKNOWN USER is not part of default privileges"); revoke.setIgnoreUnknownUser(true); } ] + { requireAccessSyntax(revoke.getOptionFor() == null || (clause.getPrivileges() == null + ? revoke.getOptionFor() != GrantOption.Kind.GRANT : revoke.getOptionFor() == GrantOption.Kind.GRANT), "Invalid REVOKE option"); + return revoke; } +} + +AlterDefaultPrivileges AlterDefaultPrivileges(): +{ AlterDefaultPrivileges result = new AlterDefaultPrivileges(); Token token; List names; Grant grant; Revoke revoke; Token statementFirst; } +{ + AccessKeyword("PRIVILEGES") + [ ( token= | token= ) { result.setRoleKeyword(accessEnum(AlterDefaultPrivileges.RoleKeyword.class, token.image)); } + names=AccessNames() { result.setRoles(names); } ] + [ names=AccessNames() { result.setSchemas(names); } ] + { statementFirst = getToken(1); } + ( grant=AccessGrant(true) { linkStatement(grant, statementFirst); result.setGrant(grant); } + | revoke=Revoke(true) { linkStatement(revoke, statementFirst); result.setRevoke(revoke); } ) + { return result; } +} + +TriggerEvent PostgreSqlTriggerEvent(): +{ TriggerEvent result = null; String name; ExpressionList columns; } +{ + ( + { result = new TriggerEvent(CreateTrigger.Event.INSERT); } + | { result = new TriggerEvent(CreateTrigger.Event.DELETE); } + | { result = new TriggerEvent(CreateTrigger.Event.TRUNCATE); } + | { result = new TriggerEvent(CreateTrigger.Event.UPDATE); } + [ { columns = new ExpressionList(); } + name=RelObjectName() { columns.add(linkToLastToken(createColumn(name))); } + ( "," name=RelObjectName() { columns.add(linkToLastToken(createColumn(name))); } )* + { result.setColumns(linkElements(columns)); } ] + ) + { return result; } +} + +TransitionRelation PostgreSqlTransitionRelation(): +{ TransitionRelation result = new TransitionRelation(); Token token; String name; } +{ + ( token= ) { result.setImage(accessEnum(TransitionRelation.Image.class, token.image)); } + [ { result.setUseAs(true); } ] name=RelObjectName() { result.setName(name); return result; } +} + +/** PostgreSQL ALTER TRIGGER name ON table RENAME TO or [NO] DEPENDS ON EXTENSION. */ +AlterTrigger AlterTrigger(): +{ + AlterTrigger result = new AlterTrigger(); + String name; + Table table; +} +{ + name=RelObjectName() { result.setName(name); } + table=Table() { result.setTable(table); } + ( + name=RelObjectName() { result.setNewName(name); } + | + LOOKAHEAD({ isExtensionDependencyAhead(1) }) + [ { result.setNoDependency(true); } ] + ContextualKeyword("DEPENDS") ContextualKeyword("EXTENSION") name=RelObjectName() + { result.setAction(AlterTrigger.Action.DEPENDS_ON_EXTENSION); result.setExtensionName(name); } + ) + { return result; } +} + +CreateTrigger PostgreSqlTrigger(boolean replace): +{ + CreateTrigger result = new CreateTrigger(); String name; Table table; CreateTrigger.Timing timing; + TriggerEvent event; TransitionRelation transition; Expression expression; Token token; boolean negative = false; + Statement body = null; + Function routine = new Function(); ExpressionList arguments = new ExpressionList(); +} +{ + [ { result.setConstraint(true); } ] + name=RelObjectName() { result.setTrigger(createTable(Collections.singletonList(name))); result.setOrReplace(replace); } + ( + LOOKAHEAD({ isAccessKeywordAhead("INSTEAD") }) AccessKeyword("INSTEAD") { result.setTiming(CreateTrigger.Timing.INSTEAD_OF); } + | timing=MySqlTriggerTiming() { result.setTiming(timing); } + ) + event=PostgreSqlTriggerEvent() { result.getEvents().add(event); } + ( event=PostgreSqlTriggerEvent() { result.getEvents().add(event); } )* + table=Table() { result.setTable(table); } + // greedy: FROM here names the referenced table, it never starts the trigger body + [ LOOKAHEAD(2) table=Table() { result.setReferencedTable(table); } ] + [ [ { negative = true; } ] { result.getConstraintAttributes().setDeferrable(!negative); } ] + [ LOOKAHEAD({ isAccessKeywordAhead("INITIALLY") }) + AccessKeyword("INITIALLY") ( token= ) + { result.getConstraintAttributes().setInitially(accessEnum(ConstraintAttributes.Initially.class, token.image)); } ] + [ LOOKAHEAD({ isAccessKeywordAhead("REFERENCING") }) AccessKeyword("REFERENCING") + transition=PostgreSqlTransitionRelation() { result.getTransitionRelations().add(transition); } + ( LOOKAHEAD({ isAccessKeywordAhead("OLD") || isAccessKeywordAhead("NEW") }) + transition=PostgreSqlTransitionRelation() { result.getTransitionRelations().add(transition); } )* ] + [ [ LOOKAHEAD({ isAccessKeywordAhead("EACH") }) AccessKeyword("EACH") { result.setUseEach(true); } ] + ( { result.setOrientation(CreateTrigger.Orientation.ROW); } + | { result.setOrientation(CreateTrigger.Orientation.STATEMENT); } ) ] + [ "(" expression=Expression() ")" { result.setWhenExpression(expression); } ] + ( + LOOKAHEAD(2) + ( | { result.setExecuteKeyword(CreateTrigger.ExecuteKeyword.PROCEDURE); } ) + name=AccessQualifiedName() { routine.setName(name); } + "(" [ expression=PostgreSqlTriggerArgument() { arguments.add(expression); } + ( "," expression=PostgreSqlTriggerArgument() { arguments.add(expression); } )* ] ")" + { routine.setParameters(arguments); result.setRoutine(routine); } + | + // DuckDB 2.0 runs a statement rather than a function + body=SingleStatement() { result.setBody(body); } + ) + { + requireAccessSyntax(!result.isConstraint() || !replace && result.getTiming() == CreateTrigger.Timing.AFTER + && result.getOrientation() == CreateTrigger.Orientation.ROW, "A constraint trigger requires AFTER FOR EACH ROW without OR REPLACE"); + requireAccessSyntax(result.isConstraint() || result.getReferencedTable() == null + && result.getConstraintAttributes().getDeferrable() == null && result.getConstraintAttributes().getInitially() == null, + "FROM and deferrability require a constraint trigger"); + return result; + } +} + +Expression PostgreSqlTriggerArgument(): +{ Expression result; Token token; String name; } +{ + ( + token= { result = stringValue(token); } + | token= { result = new LongValue(token.image); } + | token= { result = new DoubleValue(token.image); } + | name=RelObjectName() { result = linkToLastToken(createColumn(name)); } + ) + { return result; } +} + +/** + * Parses PostgreSQL CREATE EVENT TRIGGER. The routine invocation matches CREATE TRIGGER but takes + * no arguments; the event and filter names stay ordinary identifiers. + */ +CreateEventTrigger CreateEventTrigger(): +{ + CreateEventTrigger statement = new CreateEventTrigger(); + CreateEventTrigger.Filter filter; + Function function = new Function(); + String name; +} +{ + name=RelObjectName() { statement.setName(name); } + name=RelObjectName() { statement.setEvent(name); } + [ filter=EventTriggerFilter() { statement.addFilters(filter); } + ( filter=EventTriggerFilter() { statement.addFilters(filter); } )* ] + ( | { statement.setExecuteKeyword(CreateTrigger.ExecuteKeyword.PROCEDURE); } ) + name=AccessQualifiedName() "(" ")" + { + function.setName(name); + function.setParameters(new ExpressionList()); + statement.setFunction(function); + return statement; + } +} + +CreateEventTrigger.Filter EventTriggerFilter(): +{ CreateEventTrigger.Filter filter = new CreateEventTrigger.Filter(); String name; Token value; } +{ + name=RelObjectName() { filter.setVariable(name); } + "(" value= { filter.addValues(stringValue(value)); } + ( "," value= { filter.addValues(stringValue(value)); } )* ")" + { return filter; } +} + +/** Event trigger states reuse the enable states of table trigger and rule actions. */ +AlterEventTrigger AlterEventTrigger(): +{ AlterEventTrigger statement = new AlterEventTrigger(); RelationAlterAction.EnableState state; String name; } +{ + name=RelObjectName() { statement.setName(name); } + ( + state=PostgreSqlEnableState() { statement.setEnableState(state); } + | ContextualKeyword("OWNER") name=RelObjectName() { statement.setOwner(name); } + | name=RelObjectName() { statement.setNewName(name); } + ) + { return statement; } +} + +List UsersList(): +{ + List users = new ArrayList(); + String user = null; +} +{ + user=RelObjectName() { users.add(user); } + ( "," user=ColumnsNamesListItem() { users.add(user); } )* + { + return users; + } +} + +void readGrantTypes(ArrayList privileges): +{ +} +{ + {privileges.add("SELECT");} | + {privileges.add("INSERT");} | + {privileges.add("UPDATE");} | + {privileges.add("DELETE");} | + {privileges.add("EXECUTE");} | + {privileges.add("ALTER");} | + {privileges.add("DROP");} +} + +Sequence Sequence() #Sequence : +{ + ObjectNames data = null; + String serverName = null, databaseName = null, schemaName = null, sequenceName = null; +} +{ + data = RelObjectNames() + { + Sequence sequence = new Sequence(data.getNames()); + linkAST(sequence,jjtThis); + return sequence; + } +} + +List SequenceParameters(): +{ + List parameters = new ArrayList(); + Sequence.Parameter parameter; +} +{ + ( LOOKAHEAD(2) parameter=SequenceParameter() { parameters.add(parameter); } )* + { return parameters; } } -/** - * Parses all RENAME variants within ALTER TABLE. - * Handles: RENAME [COLUMN] old TO new, RENAME TO tablename, - * RENAME INDEX/KEY/CONSTRAINT old TO new. - */ -AlterExpression AlterExpressionRenameOp(): +Sequence.Parameter SequenceParameter(): { - AlterExpression alterExp = new AlterExpressionRename(); - Token tk; - Token tk2; - Index index; + Sequence.ParameterType type; + Long value = null; + Token token; + boolean withKeyword = false; } { - ( - LOOKAHEAD(2) ( - ( - { alterExp.setOperation(AlterOperation.RENAME_INDEX); } - | { alterExp.setOperation(AlterOperation.RENAME_KEY); } - ) - | - { alterExp.setOperation(AlterOperation.RENAME_CONSTRAINT); } - ) - (tk= | tk=) { - alterExp.setOldIndex(new Index().withName(tk.image)); - } - - (tk2= | tk2=) { - index = new Index().withName(tk2.image); - alterExp.setIndex(index); - } + [ { withKeyword = true; } ] value=SequenceParameterValue() + { type = withKeyword ? Sequence.ParameterType.INCREMENT_BY : Sequence.ParameterType.INCREMENT; } | - LOOKAHEAD(2) ( - { alterExp.setOperation(AlterOperation.RENAME_TABLE); } - (tk2= | tk2=) { alterExp.setNewTableName(tk2.image); } - ) + [ { withKeyword = true; } ] value=SequenceParameterValue() + { type = withKeyword ? Sequence.ParameterType.START_WITH : Sequence.ParameterType.START; } | - ( - { alterExp.setOperation(AlterOperation.RENAME); } - [ { alterExp.hasColumn(true); } ] - (tk=KeywordOrIdentifier()) { alterExp.setColOldName(tk.image); } - - (tk2=KeywordOrIdentifier()) { alterExp.setColumnName(tk2.image); } - ) + [ LOOKAHEAD(2) value=SequenceParameterValue() ] + { type = Sequence.ParameterType.RESTART_WITH; } + | + ( token= | token= | token= ) value=SequenceParameterValue() + { type = Sequence.ParameterType.valueOf(token.image.toUpperCase(Locale.ROOT)); } + | + ( token= | token= | token= ) + { type = Sequence.ParameterType.valueOf("NO_" + token.image.toUpperCase(Locale.ROOT)); } + | + ( token= | token= | token= | token= + | token= | token= | token= | token= + | token= | token= | token= ) + { type = Sequence.ParameterType.valueOf(token.image.toUpperCase(Locale.ROOT)); } ) - { return alterExp; } + { return new Sequence.Parameter(type).withValue(value); } } -/** -* Dispatcher production for all ALTER TABLE expression types. -* Delegates to focused sub-productions for each operation category. -*/ -AlterExpression AlterExpression(): +Long SequenceParameterValue(): { - AlterExpression alterExp = null; - Token tk; - Token tk2 = null; - String sk3 = null; + Token sign = null; + Token value; +} +{ + [ sign="-" | sign="+" ] value= + { return Long.valueOf((sign == null ? "" : sign.image) + value.image); } +} - // for captureRest() - List tokens = new LinkedList(); +SequenceOwnership SequenceOwnership(): +{ + Token token; + Column column; + SequenceOwnership ownership; } { + token= { + requireDdlSyntax("OWNED".equalsIgnoreCase(token.image), "Expected OWNED"); + } + + ( LOOKAHEAD(1) { ownership = SequenceOwnership.none(); } + | column=Column() { ownership = SequenceOwnership.ownedBy(column); } ) + { return ownership; } +} +/** AS, ownership and numeric parameters share the same sequence-option boundary. */ +void SequenceDefinitionOptions(Sequence sequence): +{ + List parameters = new ArrayList(); + Sequence.Parameter parameter; + SequenceOwnership ownership; + Token dataType; +} +{ ( - alterExp = AlterExpressionAddAlterModify() - | - ( - { alterExp = new AlterExpression(); } - { alterExp.setOperation(AlterOperation.CHANGE); } - [ { alterExp.hasColumn(true); alterExp.setOptionalSpecifier("COLUMN"); } ] - ( - { AlterExpression.ColumnDataType alterExpressionColumnDataType; } - (tk=KeywordOrIdentifier()) - alterExpressionColumnDataType = AlterExpressionColumnDataType() { alterExp.withColumnOldName(tk.image).addColDataType(alterExpressionColumnDataType); } - ) - ) - | - alterExp = AlterExpressionDrop() - | - LOOKAHEAD(5) ( - { alterExp = new AlterExpression(); } - { - alterExp.setOperation(AlterOperation.FORCE_ROW_LEVEL_SECURITY); - } - ) - | - LOOKAHEAD(5) ( - { alterExp = new AlterExpression(); } - { - alterExp.setOperation(AlterOperation.NO_FORCE_ROW_LEVEL_SECURITY); - } - ) - | - LOOKAHEAD(1) ( - { alterExp = new AlterExpression(); } - { alterExp.setOperation(AlterOperation.FORCE); } - ) - | - ( - { alterExp = new AlterExpressionTableOption(); } - { alterExp.setOperation(AlterOperation.ALGORITHM); } - ["=" { alterExp.setUseEqual(true);} ] - sk3 = RelObjectName() { alterExp.setAlgorithmOption(sk3); } - ) - | - ( - { alterExp = new AlterExpressionTableOption(); } - { alterExp.setOperation(AlterOperation.KEY_BLOCK_SIZE); } - ["=" { alterExp.setUseEqual(true);} ] - tk= { alterExp.setKeyBlockSize(Integer.parseInt(tk.image)); } - ) - | - ( - { alterExp = new AlterExpressionTableOption(); } - { alterExp.setOperation(AlterOperation.LOCK); } - ["=" { alterExp.setUseEqual(true);} ] - sk3 = RelObjectName() { alterExp.setLockOption(sk3); } - ) - | - ( - { alterExp = new AlterExpressionTableOption(); } - { alterExp.setOperation(AlterOperation.ENGINE); } - ["=" { alterExp.setUseEqual(true);} ] - sk3 = RelObjectName() { alterExp.setEngineOption(sk3); } - ) - | - LOOKAHEAD(2) alterExp = AlterExpressionRenameOp() - | - ({ alterExp = new AlterExpressionCharset(); } - { - alterExp.setOperation(AlterOperation.CONVERT); - alterExp.setConvertType(AlterExpression.ConvertType.CONVERT_TO); - } - tk= { alterExp.setCharacterSet(tk.image); } - [ tk2= { alterExp.setCollation(tk2.image); }] - ) - | - LOOKAHEAD(3) + LOOKAHEAD(2) ( - { alterExp = new AlterExpressionCharset(); } - - ( - [ "=" { alterExp.setHasEqualForCharacterSet(true); } ] - tk= { - alterExp.setOperation(AlterOperation.CONVERT); - alterExp.setConvertType(AlterExpression.ConvertType.DEFAULT_CHARACTER_SET); - alterExp.setCharacterSet(tk.image); - } - [ [ "=" { alterExp.setHasEqualForCollate(true); } ] - tk2= { alterExp.setCollation(tk2.image); }] - | - - [ "=" { alterExp.setHasEqualForCollate(true); } ] - tk= { - alterExp.setOperation(AlterOperation.COLLATE); - alterExp.setCollation(tk.image); - alterExp.setDefaultCollateSpecified(true); - } - ) - ) - | - ({ alterExp = new AlterExpressionCharset(); } - [ "=" { alterExp.setHasEqualForCharacterSet(true); } ] - tk= { - alterExp.setOperation(AlterOperation.CONVERT); - alterExp.setConvertType(AlterExpression.ConvertType.CHARACTER_SET); - alterExp.setCharacterSet(tk.image); - } - [ [ "=" { alterExp.setHasEqualForCollate(true); } ] - tk2= { alterExp.setCollation(tk2.image); }] - ) - | - ({ alterExp = new AlterExpressionCharset(); } - { alterExp.setOperation(AlterOperation.COLLATE); } - [ "=" { alterExp.setHasEqualForCollate(true); } ] - tk= { alterExp.setCollation(tk.image); } - ) - | - ({ alterExp = new AlterExpressionTableOption(); } - {alterExp.setOperation(AlterOperation.COMMENT);} - ["=" {alterExp.setOperation(AlterOperation.COMMENT_WITH_EQUAL_SIGN);} ] - tk= { alterExp.setCommentText(tk.image); } - ) - | - ({ alterExp = new AlterExpressionTableOption(); } - {alterExp.setOperation(AlterOperation.SET_TABLE_OPTION);} - ["=" { alterExp.setUseEqual(true);} ] - tk= { - if (alterExp.getUseEqual()) { - alterExp.setTableOption("ENCRYPTION = " + tk.image); - } else { - alterExp.setTableOption("ENCRYPTION " + tk.image); - } - } - ) - | - alterExp = AlterExpressionDiscardOrImport() - | - LOOKAHEAD(4) ( - { alterExp = new AlterExpression(); } - { - alterExp.setOperation(AlterOperation.DISABLE_ROW_LEVEL_SECURITY); - } - ) - | - LOOKAHEAD(2) ( - { alterExp = new AlterExpressionTableOption(); } - { - alterExp.setOperation(AlterOperation.DISABLE_KEYS); - } - ) - | - LOOKAHEAD(4) ( - { alterExp = new AlterExpression(); } - { - alterExp.setOperation(AlterOperation.ENABLE_ROW_LEVEL_SECURITY); - } - ) - | - LOOKAHEAD(2) ( - { alterExp = new AlterExpressionTableOption(); } - { - alterExp.setOperation(AlterOperation.ENABLE_KEYS); + ( dataType= | dataType= ) { + requireDdlSyntax(sequence.getDataType() == null, "Duplicate sequence AS clause"); + sequence.setDataType(dataType.image); } - ) - | - ({ alterExp = new AlterExpressionTableOption(); } - {alterExp.setOperation(AlterOperation.SET_TABLE_OPTION);} - ["=" { alterExp.setUseEqual(true);} ] - tk= { - if (alterExp.getUseEqual()) { - alterExp.setTableOption("AUTO_INCREMENT = " + tk.image); - } else { - alterExp.setTableOption("AUTO_INCREMENT " + tk.image); - } + | + LOOKAHEAD({ isKeywordAhead("OWNED") }) ownership=SequenceOwnership() { + requireDdlSyntax(sequence.getOwnership() == null, "Duplicate sequence OWNED BY clause"); + sequence.setOwnership(ownership); } + | + parameter=SequenceParameter() { parameters.add(parameter); } ) - | - alterExp = AlterExpressionPartitionOp() - | - { alterExp = new AlterExpression(); } - tokens = captureRest() { - alterExp.setOperation(AlterOperation.UNSPECIFIC); - StringBuilder optionalSpecifier = new StringBuilder(); - int i=0; - - for (String s: tokens) - if (! (s.equals(";") || s.equals("\n\n\n")) ) { - if (i>0) - optionalSpecifier.append( " " ); - optionalSpecifier.append( s ); - i++; - } - - alterExp.setOptionalSpecifier( optionalSpecifier.toString() ); - } - ) - + )* { - return alterExp; + sequence.setParameters(parameters); + if (!parameters.isEmpty() || sequence.getOwnership() != null) { + widen(sequence, null, getToken(0)); + } } } -Statement Alter(): + +CreateSequence CreateSequence(): { - Statement statement; - List captureRest; + CreateSequence result = new CreateSequence(); + Sequence sequence; + Token persistence; + Token scope = null; +} +{ + [ + ( { result.setPersistence(CreateSequence.Persistence.UNLOGGED); } + | + [ ( scope= | scope= ) ] + ( persistence= | persistence= ) { + result.setPersistence(CreateSequence.Persistence.valueOf( + (scope == null ? "" : scope.image.toUpperCase(Locale.ROOT) + "_") + + persistence.image.toUpperCase(Locale.ROOT))); + } ) + ] + + [ LOOKAHEAD(3) { result.setIfNotExists(true); } ] + sequence=Sequence() { result.setSequence(sequence); } + SequenceDefinitionOptions(sequence) + { return result; } +} + +AlterSequence AlterSequence(): +{ + AlterSequence result = new AlterSequence(); + Sequence sequence; + String name; } { + [ LOOKAHEAD(2) { result.setIfExists(true); } ] + sequence=Sequence() { result.setSequence(sequence); } ( - ( - - ( - statement = AlterTable() - | - statement = AlterSession() - | - statement = AlterView(false) - | - statement = AlterSystemStatement() - | - statement = AlterSequence() - | - captureRest = captureRest() - { - statement = new UnsupportedStatement("ALTER", captureRest); - } - ) - ) - | - ( - - ( - statement = AlterView(true) - | - captureRest = captureRest() - { - statement = new UnsupportedStatement("REPLACE", captureRest); - } - ) + name=RelObjectName() + { result.setAction(AlterSequence.Action.RENAME); result.setNewName(name); } + | + LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { result.setAction(AlterSequence.Action.OWNER); result.setOwner(name); } + | + + ( + name=RelObjectName() + { result.setAction(AlterSequence.Action.SET_SCHEMA); result.setSchemaName(name); } + | { result.setAction(AlterSequence.Action.SET_UNLOGGED); } + | ContextualKeyword("LOGGED") { result.setAction(AlterSequence.Action.SET_LOGGED); } ) + | + SequenceDefinitionOptions(sequence) + ) + { return result; } +} + +Statement Create(): +{ + boolean isUsingOrReplace=false; + Token tk; + Statement statement; + List captureRest; +} +{ + [ { isUsingOrReplace = true; } ] + ( + LOOKAHEAD({ isKeywordAhead("ACCESS") && "METHOD".equalsIgnoreCase(getToken(2).image) }) + { requireDdlSyntax(!isUsingOrReplace, "CREATE ACCESS METHOD does not support OR REPLACE"); } + statement = CreateAccessMethod() + | + LOOKAHEAD( ) { requireDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for foreign-data objects"); } statement = CreateForeignDataWrapper() + | + LOOKAHEAD({ isKeywordAhead("SERVER") }) { requireDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for foreign-data objects"); } statement = CreateServer() + | + LOOKAHEAD({ getToken(1).kind == K_USER && "MAPPING".equalsIgnoreCase(getToken(2).image) }) { requireDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for foreign-data objects"); } statement = CreateUserMapping() + | + LOOKAHEAD({ getToken(1).kind == K_TEXT_LITERAL && getToken(2).kind == K_SEARCH + && "CONFIGURATION".equalsIgnoreCase(getToken(3).image) }) + { requireDdlSyntax(!isUsingOrReplace, "CREATE TEXT SEARCH CONFIGURATION does not support OR REPLACE"); } statement = CreateTextSearchConfiguration() + | + LOOKAHEAD({ isKeywordAhead("RULE") }) statement = CreateRule(isUsingOrReplace) + | + LOOKAHEAD({ isKeywordAhead("COLLATION") }) { requireDdlSyntax(!isUsingOrReplace, "CREATE COLLATION does not support OR REPLACE"); } statement = CreateCollation() + | + LOOKAHEAD({ getToken(1).kind == K_TABLESPACE + || isKeywordAhead("UNDO") && getToken(2).kind == K_TABLESPACE }) + { requireDdlSyntax(!isUsingOrReplace, "CREATE TABLESPACE does not support OR REPLACE"); } + statement = CreateTablespace() + | + LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = CreateStatistics() + | + LOOKAHEAD({ isMySqlCreateRoleAhead() }) statement = MySqlCreateRole() + { requireAccessSyntax(!isUsingOrReplace, "CREATE ROLE does not accept OR REPLACE"); } + | + LOOKAHEAD({ isPostgreSqlRoleAhead() }) statement = CreateRole() + { requireAccessSyntax(!isUsingOrReplace, "CREATE ROLE does not accept OR REPLACE"); } + | + LOOKAHEAD({ isPostgreSqlTriggerAhead() }) statement = PostgreSqlTrigger(isUsingOrReplace) + | + LOOKAHEAD(1, ) { requireTypeDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for CREATE TYPE"); } statement = CreateType() + | + LOOKAHEAD(1, ) { requireTypeDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for CREATE DOMAIN"); } statement = CreateDomain() + | + LOOKAHEAD({ isKeywordAhead("EXTENSION") + && "REPOSITORY".equalsIgnoreCase(getToken(2).image) }) + { requireTypeDdlSyntax(!isUsingOrReplace, + "OR REPLACE is not supported for CREATE EXTENSION REPOSITORY"); } + statement = CreateExtensionRepository() + | + LOOKAHEAD({ isKeywordAhead("EXTENSION") }) { requireTypeDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for CREATE EXTENSION"); } statement = CreateExtension() + | + LOOKAHEAD({ isKeywordAhead("PUBLICATION") }) + { requireReplicationSyntax(!isUsingOrReplace, "OR REPLACE is not supported for CREATE PUBLICATION"); } + statement = CreatePublication() + | + LOOKAHEAD({ isKeywordAhead("SUBSCRIPTION") }) + { requireReplicationSyntax(!isUsingOrReplace, "OR REPLACE is not supported for CREATE SUBSCRIPTION"); } + statement = CreateSubscription() + | + statement = CreateUser() + | + LOOKAHEAD({ isPostgreSqlEventTriggerAhead(false) }) + { requireDdlSyntax(!isUsingOrReplace, "CREATE EVENT TRIGGER does not support OR REPLACE"); } + statement = CreateEventTrigger() + | + LOOKAHEAD([ MySqlRoutineDefiner() ] , + { getToken(1).kind == K_EVENT || isKeywordAhead("DEFINER") }) + statement = CreateEvent() + | + LOOKAHEAD(MySqlViewPrefix() ) statement = CreateView(isUsingOrReplace) + | + LOOKAHEAD([ MySqlTriggerDefiner() ] , + { getToken(1).kind == K_TRIGGER || isKeywordAhead("DEFINER") }) + statement = CreateTrigger(isUsingOrReplace) + | + LOOKAHEAD({ isCreateMacroAhead() }) statement = CreateMacro(isUsingOrReplace) + | + LOOKAHEAD({ isCreateFunctionMacroAhead() }) statement = CreateMacro(isUsingOrReplace) + | + LOOKAHEAD([ MySqlRoutineDefiner() ] ( | )) + statement = CreateFunctionStatement(isUsingOrReplace) + | + statement = CreateSchema() + | + statement = CreateDatabase() + | + LOOKAHEAD([ | [ | ] ( | ) ] ) + statement = CreateSequence() + | + statement = CreateSynonym(isUsingOrReplace) + | + LOOKAHEAD({ isUnknownCreateAhead() }) captureRest = captureRest() + { statement = unsupportedCreate(isUsingOrReplace, captureRest); } + | + LOOKAHEAD(3) statement = CreateTable(isUsingOrReplace) + | + LOOKAHEAD(2) statement = CreateView(isUsingOrReplace) + | + statement = CreatePolicy() + | + /* @fixme + * Create Index uses CreateParameter() which allows all kind of tokens + * it can conflict with other statements and must be at the end right now + */ + statement = CreateIndex() + | + captureRest = captureRest() + { statement = unsupportedCreate(isUsingOrReplace, captureRest); } ) { return statement; } } -Alter AlterTable(): +/** With a MySQL dialect, the characteristics are also parsed; the tokens still render the statement. */ +CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace): { - Alter alter = new Alter(); - Table table; - AlterExpression alterExp; - boolean usingIfExists = false; + CreateFunctionalStatement type = null; + List tokens = new LinkedList(); + String statementType = null; + MySqlDefiner definer = null; + List characteristics = new ArrayList(); } { - - [ { alter.setUseOnly(true); } ] - [ LOOKAHEAD(2) { alter.setUseTableIfExists(true); } ] - table=Table() { alter.setTable(table); } - - alterExp=AlterExpression() { alter.addAlterExpression(alterExp); } - - ("," alterExp=AlterExpression() { alter.addAlterExpression(alterExp); } )* - - { - return alter; + [ definer=MySqlRoutineDefiner() ] + ( + { statementType = "FUNCTION"; } + | + { statementType = "PROCEDURE"; } + ) + tokens=captureFunctionBody(isMySqlDialect() ? characteristics : null) + { + if(statementType.equals("FUNCTION")) { + type = new CreateFunction(isUsingOrReplace, tokens); + } + if(statementType.equals("PROCEDURE")) { + type = new CreateProcedure(isUsingOrReplace, tokens); } + type.setFunctionDeclarationParts(tokens, characteristics); + type.setDefiner(definer); + return type; + } } -AlterSession AlterSession(): +/** + * PostgreSQL ALTER FUNCTION, PROCEDURE or ROUTINE, or MySQL ALTER FUNCTION or PROCEDURE. One + * statement's characteristics belong to one dialect; MySQL's take no argument list. + */ +AlterRoutine AlterRoutine(): { - AlterSessionOperation operation = null; - List parameters = new ArrayList(); - Token token; + AlterRoutine result = new AlterRoutine(); + RoutineReference routine; + RoutineCharacteristic characteristic; + String name; } { - ( - ( - ( { operation = AlterSessionOperation.ADVISE_COMMIT; } - | { operation = AlterSessionOperation.ADVISE_ROLLBACK; } - | { operation = AlterSessionOperation.ADVISE_NOTHING; } - ) - ) + ( + { result.setRoutineType(AlterRoutine.RoutineType.FUNCTION); } | - ( - { operation = AlterSessionOperation.CLOSE_DATABASE_LINK; } - ) + { result.setRoutineType(AlterRoutine.RoutineType.PROCEDURE); } | - ( - ( { operation = AlterSessionOperation.ENABLE_COMMIT_IN_PROCEDURE; } - | { operation = AlterSessionOperation.ENABLE_GUARD; } - | ( { operation = AlterSessionOperation.ENABLE_PARALLEL_DML; } - | { operation = AlterSessionOperation.ENABLE_PARALLEL_DDL; } - | { operation = AlterSessionOperation.ENABLE_PARALLEL_QUERY; } - ) - | { operation = AlterSessionOperation.ENABLE_RESUMABLE; } - ) - ) + ContextualKeyword("ROUTINE") { result.setRoutineType(AlterRoutine.RoutineType.ROUTINE); } + ) + routine=RoutineReference(false) { result.setRoutine(routine); } + ( + name=RelObjectName() + { result.setAction(AlterRoutine.Action.RENAME); result.setNewName(name); } | - ( - ( { operation = AlterSessionOperation.DISABLE_COMMIT_IN_PROCEDURE; } - | { operation = AlterSessionOperation.DISABLE_GUARD; } - | ( { operation = AlterSessionOperation.DISABLE_PARALLEL_DML; } - | { operation = AlterSessionOperation.DISABLE_PARALLEL_DDL; } - | { operation = AlterSessionOperation.DISABLE_PARALLEL_QUERY; } - ) - | { operation = AlterSessionOperation.DISABLE_RESUMABLE; } - ) - ) + LOOKAHEAD({ isWordSequenceAhead(1, "OWNER", "TO") }) ContextualKeyword("OWNER") name=RelObjectName() + { result.setAction(AlterRoutine.Action.OWNER); result.setOwner(name); } | + LOOKAHEAD({ getToken(1).kind == K_SET && getToken(2).kind == K_SCHEMA + && !isPostgreSqlRoutineCharacteristicAhead(1) }) + name=RelObjectName() + { result.setAction(AlterRoutine.Action.SET_SCHEMA); result.setSchemaName(name); } + | + LOOKAHEAD({ isExtensionDependencyAhead(1) }) + [ { result.setNoDependency(true); } ] + ContextualKeyword("DEPENDS") ContextualKeyword("EXTENSION") name=RelObjectName() + { result.setAction(AlterRoutine.Action.DEPENDS_ON_EXTENSION); result.setExtensionName(name); } + | + LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) }) ( - ( { operation = AlterSessionOperation.FORCE_PARALLEL_DML; } - | { operation = AlterSessionOperation.FORCE_PARALLEL_DDL; } - | { operation = AlterSessionOperation.FORCE_PARALLEL_QUERY; } - ) - ) + LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) }) + characteristic=MySqlRoutineCharacteristic(false) { result.getCharacteristics().add(characteristic); } + )+ + { + requireDdlSyntax(result.getRoutineType() != AlterRoutine.RoutineType.ROUTINE + && routine.getArguments() == null, + "MySQL characteristics require ALTER FUNCTION or PROCEDURE without arguments"); + } | ( - { operation = AlterSessionOperation.SET; } - ) + LOOKAHEAD({ isPostgreSqlRoutineCharacteristicAhead(1) }) + characteristic=PostgreSqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); } + )+ + [ { result.setRestrict(true); } ] ) - - ( ( token = - | token = - | token = "=" - | token = - | token = - ) { parameters.add( token.image ); } - )* - - - { - return new AlterSession(operation, parameters); - } + { return result; } } -AlterSystemStatement AlterSystemStatement(): +RoutineCharacteristic PostgreSqlRoutineCharacteristic(): { - AlterSystemOperation operation = null; - List parameters = new LinkedList(); + RoutineCharacteristic.Kind kind = RoutineCharacteristic.Kind.CONFIGURATION_PARAMETER; + RoutineCharacteristic.SecurityClause clause = null; + ConfigurationParameterChange change = null; + String value = null; } { - ( - ( - { operation = AlterSystemOperation.ARCHIVE_LOG; } - ) + ( + change=ConfigurationParameterChange() | - ( - { operation = AlterSystemOperation.CHECKPOINT; } - ) + LOOKAHEAD({ isKeywordAhead("IMMUTABLE") }) ContextualKeyword("IMMUTABLE") + { kind = RoutineCharacteristic.Kind.IMMUTABLE; } | - ( - { operation = AlterSystemOperation.DUMP_ACTIVE_SESSION_HISTORY; } - ) + LOOKAHEAD({ isKeywordAhead("STABLE") }) ContextualKeyword("STABLE") + { kind = RoutineCharacteristic.Kind.STABLE; } | - ( - ( - "DISTRIBUTED" "RECOVERY" { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; } - | { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; } - ) - ) + { kind = RoutineCharacteristic.Kind.VOLATILE; } | - ( - ( - "DISTRIBUTED" "RECOVERY" { operation = AlterSystemOperation.DISABLE_DISTRIBUTED_RECOVERY; } - | { operation = AlterSystemOperation.DISABLE_RESTRICTED_SESSION; } - ) - ) + LOOKAHEAD({ isKeywordAhead("CALLED") }) ContextualKeyword("CALLED") ContextualKeyword("INPUT") + { kind = RoutineCharacteristic.Kind.CALLED_ON_NULL_INPUT; } | - ( - { operation = AlterSystemOperation.FLUSH; } - ) + ContextualKeyword("INPUT") + { kind = RoutineCharacteristic.Kind.RETURNS_NULL_ON_NULL_INPUT; } | - ( - { operation = AlterSystemOperation.DISCONNECT_SESSION; } - ) + { kind = RoutineCharacteristic.Kind.STRICT; } | - ( - { operation = AlterSystemOperation.KILL_SESSION; } - ) + LOOKAHEAD({ isKeywordAhead("LEAKPROOF") }) ContextualKeyword("LEAKPROOF") + { kind = RoutineCharacteristic.Kind.LEAKPROOF; } | - ( - { operation = AlterSystemOperation.SWITCH; } - ) + ContextualKeyword("LEAKPROOF") { kind = RoutineCharacteristic.Kind.NOT_LEAKPROOF; } | - ( - { operation = AlterSystemOperation.SUSPEND; } - ) + LOOKAHEAD({ isKeywordAhead("EXTERNAL") }) ContextualKeyword("EXTERNAL") + kind=RoutineSecurity() { clause = RoutineCharacteristic.SecurityClause.EXTERNAL_SECURITY; } | - ( - { operation = AlterSystemOperation.RESUME; } - ) + kind=RoutineSecurity() { clause = RoutineCharacteristic.SecurityClause.SECURITY; } | + ( - { operation = AlterSystemOperation.QUIESCE; } + LOOKAHEAD({ isKeywordAhead("UNSAFE") }) ContextualKeyword("UNSAFE") + { kind = RoutineCharacteristic.Kind.PARALLEL_UNSAFE; } + | + { kind = RoutineCharacteristic.Kind.PARALLEL_RESTRICTED; } + | + ContextualKeyword("SAFE") { kind = RoutineCharacteristic.Kind.PARALLEL_SAFE; } ) | - ( - { operation = AlterSystemOperation.UNQUIESCE; } - ) + LOOKAHEAD({ isKeywordAhead("COST") }) ContextualKeyword("COST") value=PostgreSqlNumericValue() + { kind = RoutineCharacteristic.Kind.COST; } | - ( - { operation = AlterSystemOperation.SHUTDOWN; } - ) + value=PostgreSqlNumericValue() { kind = RoutineCharacteristic.Kind.ROWS; } | - ( - { operation = AlterSystemOperation.REGISTER; } - ) + LOOKAHEAD({ isKeywordAhead("SUPPORT") }) ContextualKeyword("SUPPORT") value=AccessQualifiedName() + { kind = RoutineCharacteristic.Kind.SUPPORT; } + ) + { + return change != null ? new RoutineCharacteristic(change) + : new RoutineCharacteristic(kind, value).withSecurityClause(clause); + } +} + +/** MySQL routine characteristic; only CREATE accepts [NOT] DETERMINISTIC. */ +RoutineCharacteristic MySqlRoutineCharacteristic(boolean create): +{ + RoutineCharacteristic.Kind kind; + RoutineCharacteristic.SecurityClause clause = null; + Token token; + StringValue comment = null; +} +{ + ( + token= + { kind = RoutineCharacteristic.Kind.COMMENT; comment = stringValue(token); } | - ( - { operation = AlterSystemOperation.SET; } - ) + LOOKAHEAD({ isKeywordAhead("LANGUAGE") }) ContextualKeyword("LANGUAGE") ContextualKeyword("SQL") + { kind = RoutineCharacteristic.Kind.LANGUAGE_SQL; } | - ( - { operation = AlterSystemOperation.RESET; } - ) + LOOKAHEAD({ isKeywordAhead("CONTAINS") }) ContextualKeyword("CONTAINS") ContextualKeyword("SQL") + { kind = RoutineCharacteristic.Kind.CONTAINS_SQL; } + | + ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.NO_SQL; } + | + LOOKAHEAD({ create && isWordSequenceAhead(1, "DETERMINISTIC") }) ContextualKeyword("DETERMINISTIC") + { kind = RoutineCharacteristic.Kind.DETERMINISTIC; } + | + LOOKAHEAD({ create && isWordSequenceAhead(1, "NOT", "DETERMINISTIC") }) + ContextualKeyword("DETERMINISTIC") { kind = RoutineCharacteristic.Kind.NOT_DETERMINISTIC; } + | + LOOKAHEAD({ isKeywordAhead("READS") }) ContextualKeyword("READS") ContextualKeyword("SQL") + { kind = RoutineCharacteristic.Kind.READS_SQL_DATA; } + | + LOOKAHEAD({ isKeywordAhead("MODIFIES") }) ContextualKeyword("MODIFIES") ContextualKeyword("SQL") + { kind = RoutineCharacteristic.Kind.MODIFIES_SQL_DATA; } + | + ContextualKeyword("SQL") kind=RoutineSecurity() + { clause = RoutineCharacteristic.SecurityClause.SQL_SECURITY; } + ) + { + return new RoutineCharacteristic(kind).withComment(comment).withSecurityClause(clause); + } +} + +RoutineCharacteristic.Kind RoutineSecurity(): +{ RoutineCharacteristic.Kind kind; } +{ + ( + LOOKAHEAD({ isKeywordAhead("DEFINER") }) ContextualKeyword("DEFINER") + { kind = RoutineCharacteristic.Kind.SECURITY_DEFINER; } + | + ContextualKeyword("INVOKER") { kind = RoutineCharacteristic.Kind.SECURITY_INVOKER; } + ) + { return kind; } +} + +/** PostgreSQL NumericOnly: an optionally signed integer or decimal, as written. */ +String PostgreSqlNumericValue(): +{ + Token sign = null; + Token number; +} +{ + [ sign="+" | sign="-" ] ( number= | number= ) + { return (sign == null ? "" : sign.image) + number.image; } +} + +CreateFunctionalStatement SqlServerRoutine(): +{ + CreateFunctionalStatement result; + CreateFunctionalStatement.Operation operation = CreateFunctionalStatement.Operation.CREATE; + List parts; + FunctionReturnType returnType; +} +{ + ( [ { operation = CreateFunctionalStatement.Operation.CREATE_OR_ALTER; } ] + | { operation = CreateFunctionalStatement.Operation.ALTER; } ) + ( + parts=captureSqlServerFunctionHeader() + returnType=SqlServerFunctionReturnType() + { result = new CreateFunction(parts); result.setReturnType(returnType); } + parts=captureSqlServerRoutineBody(false) { result.setRoutineBodyParts(parts); } + | parts=captureSqlServerRoutineBody(true) { result = new CreateProcedure(parts); } ) - parameters = captureRest() + { result.setOperation(operation); return result; } +} - { - return new AlterSystemStatement(operation, parameters); +FunctionReturnType SqlServerFunctionReturnType(): +{ + FunctionReturnType result = new FunctionReturnType(); + UserVariable variable; ColDataType dataType; + TableElement element; List elements = new ArrayList(); +} +{ + + ( + LOOKAHEAD({ getToken(1).kind == K_TABLE || getToken(1).kind == S_AT_IDENTIFIER }) + [ variable=UserVariable() { result.setTableVariable(variable.toString()); } ] + { result.setTable(true); } + [ "(" element=SqlServerReturnTableElement() { elements.add(element); } + ( "," element=SqlServerReturnTableElement() { elements.add(element); } )* ")" + { result.setTableElements(elements); } ] + { requireDdlSyntax(result.getTableVariable() == null || result.getTableElements() != null, + "A return table variable requires column definitions"); } + | dataType=ColDataType() { result.setDataType(dataType); } + ) + { return result; } +} + +TableElement SqlServerReturnTableElement(): +{ TableElement element; } +{ + ( LOOKAHEAD(3) element=CreateTableConstraint() | element=ColumnDefinition() ) + { return element; } +} + +JAVACODE +List captureSqlServerFunctionHeader() { + List parts = new ArrayList(); + int depth = 0; + while (true) { + Token next = getToken(1); + if (next.kind == K_RETURNS && depth == 0) break; + if (next.kind == EOF || next.kind == ST_SEMICOLON) { + throw new ParseException("A SQL Server function requires RETURNS"); + } + if ("(".equals(next.image)) depth++; + if (")".equals(next.image)) depth--; + if (depth < 0) throw new ParseException("Unbalanced function parameters"); + parts.add(getNextToken().image); } + if (parts.size() < 3 || "(".equals(parts.get(0)) || !")".equals(parts.get(parts.size() - 1))) { + throw new ParseException("A SQL Server function requires a name and parameter parentheses"); + } + return parts; } -Wait Wait(): +JAVACODE +List captureSqlServerRoutineBody(boolean procedure) { + List parts = new ArrayList(); + int depth = 0; + while (true) { + Token next = getToken(1); + // Procedure definitions occupy the rest of their batch, including statements + // after an END. Their opaque bodies may contain BEGIN TRANSACTION and TRY/CATCH. + if (next.kind == EOF || (!procedure && next.kind == ST_SEMICOLON && depth == 0)) break; + if (!procedure) { + if (next.kind == K_BEGIN || next.kind == K_CASE) { + depth++; + } else if (next.kind == K_END) { + if (--depth < 0) throw new ParseException("Unmatched END in routine"); + } + } + parts.add(getNextToken().image); + } + if (depth != 0) throw new ParseException("Unterminated SQL Server routine block"); + if (parts.isEmpty()) throw new ParseException("A routine body is required"); + return parts; +} + +CreateSynonym CreateSynonym(boolean isUsingOrReplace): { - Wait wait = new Wait(); - Token token = null; + CreateSynonym createSynonym = new CreateSynonym(); + Synonym synonym; + boolean publicSynonym = false; + ObjectNames data = null; } { - // sqlserver-oracle-> WAIT (TIMEOUT) - // https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10002.htm#i2126016 - token= - { - wait.setTimeout(Long.parseLong(token.image)); - return wait; - } + [ { publicSynonym = true; } ] + synonym=Synonym() { createSynonym.setSynonym(synonym); } + data = RelObjectNames() + { + createSynonym.setOrReplace(isUsingOrReplace); + createSynonym.setPublicSynonym(publicSynonym); + createSynonym.setForList(data.getNames()); + return createSynonym; + } } -SavepointStatement SavepointStatement(): +Synonym Synonym() #Synonym : { - SavepointStatement savepointStatement; + ObjectNames data = null; + String serverName = null, databaseName = null, schemaName = null, sequenceName = null; } { - token= + data = RelObjectNames() { - savepointStatement = new SavepointStatement(token.image); - return savepointStatement; + Synonym synonym = new Synonym(data.getNames()); + linkAST(synonym,jjtThis); + return synonym; } } -RollbackStatement RollbackStatement(): + +String TypeDdlName(): +{ String name; String part; } { - RollbackStatement rollbackStatement; - boolean usingWorkKeyword=false; - boolean usingSavepointKeyword=false; - String savepointName=null; - String forceDistributedTransactionIdentifier=null; + name=RelObjectName() + ( LOOKAHEAD(2) "." part=RelObjectNameExt() { name += "." + part; } )* + { return name; } } + +void TypeDdlKeyword(String expected): +{ Token keyword; } +{ ( keyword= | keyword= ) { requireTypeDdlSyntax(expected.equalsIgnoreCase(keyword.image), "Expected " + expected); } } + +Expression ExtensionVersion(): +{ Token token; String name; Expression result; } { - { rollbackStatement = new RollbackStatement(); } - [ { rollbackStatement.setUsingWorkKeyword(true); } ] - [ ( - [ { rollbackStatement.setUsingSavepointKeyword(true); }] - token= { rollbackStatement.setSavepointName(token.image); } - ) - | + ( token= { result = stringValue(token); } + | name=RelObjectName() { result = linkToLastToken(createColumn(name)); } ) + { return result; } +} + +TypeAttribute TypeAttribute(): +{ TypeAttribute attribute = new TypeAttribute(); String name; ColDataType type; } +{ + name=RelObjectName() { attribute.setName(name); } + type=ColDataType() { attribute.setDataType(type); } + [ name=TypeDdlName() { attribute.setCollation(name); } ] + { return attribute; } +} + +CreateType CreateType(): +{ + CreateType result = new CreateType(); + String name; Token label; TypeAttribute attribute; + EnumTypeDefinition enumeration = new EnumTypeDefinition(); + CompositeTypeDefinition composite = new CompositeTypeDefinition(); + RangeTypeDefinition range = new RangeTypeDefinition(); + RangeTypeDefinition.Option option; +} +{ + name=TypeDdlName() { result.setName(name); } + [ + ( - token= { rollbackStatement.setForceDistributedTransactionIdentifier(token.image); } - ) ] + LOOKAHEAD({ isKeywordAhead("ENUM") }) TypeDdlKeyword("ENUM") "(" + [ label= { enumeration.getLabels().add(stringValue(label)); } + ( "," label= { enumeration.getLabels().add(stringValue(label)); } )* ] + ")" { result.setDefinition(enumeration); } + | "(" option=RangeTypeOption() { range.getOptions().add(option); } + ( "," option=RangeTypeOption() { range.getOptions().add(option); } )* ")" + { + requireTypeDdlSyntax(range.getSubtype() != null, "A range definition requires SUBTYPE"); + result.setDefinition(range); + } + | "(" [ attribute=TypeAttribute() { composite.getAttributes().add(attribute); } + ( "," attribute=TypeAttribute() { composite.getAttributes().add(attribute); } )* ] + ")" { result.setDefinition(composite); } + ) + ] + { return result; } +} - { - return rollbackStatement; - } +RangeTypeDefinition.Option RangeTypeOption(): +{ String name; RangeTypeDefinition.OptionKind kind; ColDataType type; } +{ + name=RelObjectName() "=" { kind = typeDdlEnum(RangeTypeDefinition.OptionKind.class, name); } + ( + LOOKAHEAD({ kind == RangeTypeDefinition.OptionKind.SUBTYPE }) + type=ColDataType() { return new RangeTypeDefinition.Option(type); } + | name=TypeDdlName() { return new RangeTypeDefinition.Option(kind, name); } + ) } -Commit Commit(): +CascadeBehavior TypeAlterBehavior(): +{ CascadeBehavior result; } { - Commit commit=new Commit(); + ( { result = CascadeBehavior.CASCADE; } + | { result = CascadeBehavior.RESTRICT; } ) + { return result; } } + +AlterType.AttributeChange TypeAttributeChange(): { - - { - return commit; - } + AlterType.AttributeChange result = new AlterType.AttributeChange(); + TypeAttribute attribute = new TypeAttribute(); + String name; ColDataType type; CascadeBehavior behavior; + boolean useSetData; +} +{ + ( + TypeDdlKeyword("ATTRIBUTE") attribute=TypeAttribute() + { result.setKind(AlterType.AttributeChange.Kind.ADD); } + | TypeDdlKeyword("ATTRIBUTE") [ LOOKAHEAD(2) { result.setIfExists(true); } ] + name=RelObjectName() { attribute.setName(name); result.setKind(AlterType.AttributeChange.Kind.DROP); } + | TypeDdlKeyword("ATTRIBUTE") name=RelObjectName() { attribute.setName(name); } + useSetData=TypeChangePrefix() { result.setUseSetData(useSetData); } + type=ColDataType() { attribute.setDataType(type); result.setKind(AlterType.AttributeChange.Kind.ALTER); } + [ name=TypeDdlName() { attribute.setCollation(name); } ] + ) + [ behavior=TypeAlterBehavior() { result.setBehavior(behavior); } ] + { result.setAttribute(attribute); return result; } } -Comment Comment(): +AlterType AlterType(): { - Comment result = new Comment(); - Table table; - Table view; - Column column; - Token comment; + AlterType result = new AlterType(); String name; Token label; + AlterType.AttributeChange change; CascadeBehavior behavior; } { - + name=TypeDdlName() { result.setName(name); } ( - ( - table = Table() { result.setTable(table); } - ) - | - ( - column = Column() { result.setColumn(column); } - ) - | - ( - view = Table() { result.setView(view); } - ) + LOOKAHEAD( ) + { result.setAction(AlterType.Action.ADD_VALUE); } + [ { result.setIfNotExists(true); } ] + label= { result.setValue(stringValue(label)); } + [ ( { result.setPosition(AlterType.Position.BEFORE); } + | TypeDdlKeyword("AFTER") { result.setPosition(AlterType.Position.AFTER); } ) + label= { result.setNeighborValue(stringValue(label)); } ] + | + ( + label= { result.setValue(stringValue(label)); } + label= { result.setNewValue(stringValue(label)); result.setAction(AlterType.Action.RENAME_VALUE); } + | LOOKAHEAD({ isKeywordAhead("ATTRIBUTE") }) TypeDdlKeyword("ATTRIBUTE") + name=RelObjectName() { result.setAttributeName(name); } + name=RelObjectName() { result.setNewName(name); result.setAction(AlterType.Action.RENAME_ATTRIBUTE); } + [ behavior=TypeAlterBehavior() { result.setBehavior(behavior); } ] + | name=RelObjectName() { result.setNewName(name); result.setAction(AlterType.Action.RENAME); } + ) + | name=RelObjectName() { result.setNewName(name); result.setAction(AlterType.Action.SET_SCHEMA); } + | LOOKAHEAD({ isKeywordAhead("OWNER") }) TypeDdlKeyword("OWNER") + name=RelObjectName() { result.setNewName(name); result.setAction(AlterType.Action.OWNER); } + | change=TypeAttributeChange() { result.setAction(AlterType.Action.ATTRIBUTES); result.getAttributeChanges().add(change); } + ( "," change=TypeAttributeChange() { result.getAttributeChanges().add(change); } )* ) - comment= - { - result.setComment(new StringValue(comment.image)); - return result; - } + { return result; } } -Grant Grant(): +DomainConstraint DomainConstraint(boolean allowNull): +{ DomainConstraint result = new DomainConstraint(); String name; Expression expression; } { - Grant grant = new Grant(); - ArrayList privileges = new ArrayList(); - List users; - Token tk = null; - ObjectNames objName; + [ name=RelObjectName() { result.setName(name); } ] + ( + { result.setKind(DomainConstraint.Kind.NOT_NULL); } + | { requireTypeDdlSyntax(allowNull, "ALTER DOMAIN ADD does not accept NULL"); result.setKind(DomainConstraint.Kind.NULL); } + | "(" expression=Expression() ")" + { result.setKind(DomainConstraint.Kind.CHECK); result.setExpression(expression); } + ) + { return result; } } + +CreateDomain CreateDomain(): +{ CreateDomain result = new CreateDomain(); String name; ColDataType type; Expression expression; DomainConstraint constraint; } { - - ( - ( - [readGrantTypes(privileges) ( readGrantTypes(privileges))*] - objName=RelObjectNames() { grant.setObjectName(objName.getNames()); } + name=TypeDdlName() { result.setName(name); } + [ { result.setUseAs(true); } ] type=ColDataType() { result.setDataType(type); } + [ name=TypeDdlName() { result.setCollation(name); } ] + [ expression=Expression() { result.setDefaultExpression(expression); } ] + // A following CHECK TABLE statement is not a domain CHECK constraint. + ( LOOKAHEAD(1, { !isTableMaintenanceAhead() }) + constraint=DomainConstraint(true) { result.getConstraints().add(constraint); } )* + { return result; } +} + +AlterDomain AlterDomain(): +{ AlterDomain result = new AlterDomain(); String name; Expression expression; DomainConstraint constraint; } +{ + name=TypeDdlName() { result.setName(name); } + ( + + ( + expression=Expression() { result.setAction(AlterDomain.Action.SET_DEFAULT); result.setDefaultExpression(expression); } + | { result.setAction(AlterDomain.Action.SET_NOT_NULL); } + | name=RelObjectName() { result.setAction(AlterDomain.Action.SET_SCHEMA); result.setNewName(name); } ) - | + | ( - tk= { grant.setRole(tk.image); } + { result.setAction(AlterDomain.Action.DROP_DEFAULT); } + | { result.setAction(AlterDomain.Action.DROP_NOT_NULL); } + | [ LOOKAHEAD(2) { result.setIfExists(true); } ] + name=RelObjectName() { result.setAction(AlterDomain.Action.DROP_CONSTRAINT); result.setConstraintName(name); } + [ { result.setBehavior(CascadeBehavior.CASCADE); } + | { result.setBehavior(CascadeBehavior.RESTRICT); } ] ) + | constraint=DomainConstraint(false) { result.setAction(AlterDomain.Action.ADD_CONSTRAINT); result.setConstraint(constraint); } + [ TypeDdlKeyword("VALID") { + requireTypeDdlSyntax(constraint.getKind() == DomainConstraint.Kind.CHECK, "NOT VALID requires a CHECK constraint"); + result.setNotValid(true); + } ] + | + ( + name=RelObjectName() { result.setConstraintName(name); } + name=RelObjectName() { result.setNewName(name); result.setAction(AlterDomain.Action.RENAME_CONSTRAINT); } + | name=RelObjectName() { result.setNewName(name); result.setAction(AlterDomain.Action.RENAME); } + ) + | + name=RelObjectName() { result.setConstraintName(name); result.setAction(AlterDomain.Action.VALIDATE_CONSTRAINT); } + | LOOKAHEAD({ isKeywordAhead("OWNER") }) TypeDdlKeyword("OWNER") + name=RelObjectName() { result.setNewName(name); result.setAction(AlterDomain.Action.OWNER); } ) - users = UsersList() - { - grant.setUsers(users); - if(privileges.size() > 0) { - grant.setPrivileges(privileges); - } - return grant; - } + { return result; } } -List UsersList(): +/** + * DuckDB 2.0's CREATE EXTENSION REPOSITORY [IF NOT EXISTS] name [WITH PREFIX prefix]. + */ +CreateExtensionRepository CreateExtensionRepository() #CreateExtensionRepository: { - List users = new ArrayList(); - String user = null; + CreateExtensionRepository createExtensionRepository = new CreateExtensionRepository(); + Expression prefix; + String name; } { - user=RelObjectName() { users.add(user); } - ( "," user=ColumnsNamesListItem() { users.add(user); } )* + TypeDdlKeyword("EXTENSION") TypeDdlKeyword("REPOSITORY") + [ LOOKAHEAD(3) + { createExtensionRepository.setIfNotExists(true); } ] + name=RelObjectName() { createExtensionRepository.setName(name); } + // greedy: WITH PREFIX belongs to the repository definition + [ + LOOKAHEAD(2) TypeDdlKeyword("PREFIX") prefix=SimpleExpression() + { createExtensionRepository.setPrefix(prefix); } + ] + { return createExtensionRepository; } +} + +CreateExtension CreateExtension(): +{ CreateExtension result = new CreateExtension(); String name; Expression version; } +{ + TypeDdlKeyword("EXTENSION") + [ LOOKAHEAD(3) { result.setIfNotExists(true); } ] + name=RelObjectName() { result.setName(name); } + [ LOOKAHEAD(1) { result.setUseWith(true); } ] + ( + name=RelObjectName() { result.getOptions().add(new CreateExtension.Option(CreateExtension.OptionKind.SCHEMA, name, null)); } + | version=ExtensionVersion() { result.getOptions().add(new CreateExtension.Option(CreateExtension.OptionKind.VERSION, null, version)); } + | { result.getOptions().add(new CreateExtension.Option(CreateExtension.OptionKind.CASCADE, null, null)); } + )* + { return result; } +} + +RoutineReference.Argument RoutineSignatureArgument(): +{ RoutineReference.Argument argument; } +{ argument=RoutineArgument(true) { return argument; } } + +/** Common argument AST; only extension aggregate signatures may end a type at ORDER BY. */ +RoutineReference.Argument RoutineArgument(boolean allowOrderBy): +{ RoutineReference.Argument result = new RoutineReference.Argument(); String name; Token mode; ColDataType type; } +{ + [ + LOOKAHEAD({ isKeywordAhead("IN") || isKeywordAhead("OUT") || isKeywordAhead("INOUT") || isKeywordAhead("VARIADIC") }) + ( { result.setMode(RoutineReference.Argument.Mode.IN); } + | mode= { result.setMode(allowOrderBy + ? typeDdlEnum(RoutineReference.Argument.Mode.class, mode.image) + : accessEnum(RoutineReference.Argument.Mode.class, mode.image)); } ) + ] + ( + LOOKAHEAD(ColDataType() ("," | ")" | )) type=ColDataType() + | name=RelObjectName() { result.setName(name); } type=ColDataType() + ) { - return users; + requireAccessSyntax(allowOrderBy || getToken(1).kind != K_ORDER, + "ORDER BY requires an aggregate signature"); + result.setDataType(type); + return result; } } -void readGrantTypes(ArrayList privileges): +RoutineReference RoutineReference(boolean aggregate): { + RoutineReference result = new RoutineReference(); String name; RoutineReference.Argument argument; + List arguments; List orderBy; } { - {privileges.add("SELECT");} | - {privileges.add("INSERT");} | - {privileges.add("UPDATE");} | - {privileges.add("DELETE");} | - {privileges.add("EXECUTE");} | - {privileges.add("ALTER");} | - {privileges.add("DROP");} + name=TypeDdlName() { result.setName(name); } + [ + LOOKAHEAD(1) + "(" { arguments = new ArrayList(); result.setArguments(arguments); } + ( + "*" { requireTypeDdlSyntax(aggregate, "Only aggregate signatures accept *"); result.setAllArguments(true); } + | + // ORDER is non-reserved, so ORDER BY without direct arguments is not an argument name + [ LOOKAHEAD(1, { getToken(1).kind != K_ORDER || getToken(2).kind != K_BY }) + argument=RoutineSignatureArgument() { arguments.add(argument); } + ( "," argument=RoutineSignatureArgument() { arguments.add(argument); } )* ] + [ { requireTypeDdlSyntax(aggregate, "ORDER BY requires an aggregate signature"); orderBy = new ArrayList(); } + argument=RoutineSignatureArgument() { orderBy.add(argument); } + ( "," argument=RoutineSignatureArgument() { orderBy.add(argument); } )* + { result.setOrderByArguments(orderBy); } ] + ) + ")" + ] + { requireTypeDdlSyntax(!aggregate || result.getArguments() != null, "An aggregate signature is required"); return result; } } -Sequence Sequence() #Sequence : +String ExtensionOperatorName(): +{ ExclusionOperator operator; } +{ operator=PostgreSqlOperatorReference() { return operator.getQualifiedName(); } } + +/** Shared schema-qualified operator spelling used in DDL references. */ +ExclusionOperator PostgreSqlOperatorReference(): +{ String schema = null; String name; } { - ObjectNames data = null; - String serverName = null, databaseName = null, schemaName = null, sequenceName = null; + [ LOOKAHEAD(RelObjectName() ".") schema=RelObjectName() "." ] + name=PostgreSqlOperatorName() + { return new ExclusionOperator().withSchemaName(schema).withName(name); } } + +/** + * The lexer splits some PostgreSQL operators, such as <<= or -|-, into several tokens. Parts that + * touch each other are joined, because PostgreSQL reads them as one operator. + */ +String PostgreSqlOperatorName(): +{ StringBuilder name = new StringBuilder(); Token token; } { - data = RelObjectNames() + token=PostgreSqlOperatorPart() { name.append(token.image); } + ( + LOOKAHEAD(1, { getToken(1).specialToken == null && getToken(1).beginLine == getToken(0).endLine + && getToken(1).beginColumn == getToken(0).endColumn + 1 }) + token=PostgreSqlOperatorPart() { name.append(token.image); } + )* + { return name.toString(); } +} + +Token PostgreSqlOperatorPart(): +{ Token token; } +{ + ( + token="+" | token="-" | token="*" | token="/" | token="%" | token= | token= + | token="<" | token=">" | token="=" | token="==" | token= | token= + | token= | token= | token= | token="*=" | token="=*" + | token= | token= | token= | token="|>" | token="&" | token= + | token= | token= | token= | token= + | token="~" | token="~*" | token="!~" | token="!~*" | token="~~" | token="~~*" | token="!~~" | token="!~~*" + | token="~~~" | token="!" | token= | token="@>" | token="<@" | token="@@" | token= + | token="?" | token="?|" | token="?&" | token="->" | token="->>" | token= | token="#>" + | token="#>>" | token= | token="-#" | token="<->" | token="<#>" + ) { - Sequence sequence = new Sequence(data.getNames()); - linkAST(sequence,jjtThis); - return sequence; + // PostgreSQL reads "< =" as two operators, while the lexer tolerates whitespace inside it + requireTypeDdlSyntax(!token.image.matches("(?s).*\\s.*"), + "Unexpected whitespace in operator " + token.image); + return token; } } -List SequenceParameters(): +ColDataType ExtensionOperatorType(): +{ ColDataType type = null; } +{ ( | type=ColDataType() ) { return type; } } + +ExtensionObject ExtensionObject(): { - List sequenceParameters = new ArrayList(); - Sequence.Parameter parameter = null; - Token token = null; - Token byToken = null; - Token withToken = null; + ExtensionObject result = new ExtensionObject(); String name; String kind; + RoutineReference routine; ColDataType type; } { ( - LOOKAHEAD(2) ( - ( - [ byToken= ] token= - { - if (byToken != null) { - parameter = new Sequence.Parameter(Sequence.ParameterType.INCREMENT_BY); - } else { - parameter = new Sequence.Parameter(Sequence.ParameterType.INCREMENT); - } - parameter.setValue(Long.parseLong(token.image)); - sequenceParameters.add(parameter); - } - ) - | - ( - [ withToken= ] token= - { - if (withToken != null) { - parameter = new Sequence.Parameter(Sequence.ParameterType.START_WITH); - } else { - parameter = new Sequence.Parameter(Sequence.ParameterType.START); - } - parameter.setValue(Long.parseLong(token.image)); - sequenceParameters.add(parameter); - } - ) - | - ( - [ LOOKAHEAD(2) token=] - { - parameter = new Sequence.Parameter(Sequence.ParameterType.RESTART_WITH); - if(token != null) { - parameter.setValue(Long.parseLong(token.image)); - } - sequenceParameters.add(parameter); - } - ) - | - - { - parameter = new Sequence.Parameter(Sequence.ParameterType.NOMAXVALUE); - sequenceParameters.add(parameter); - } - | - token= - { - parameter = new Sequence.Parameter(Sequence.ParameterType.MAXVALUE); - parameter.setValue(Long.parseLong(token.image)); - sequenceParameters.add(parameter); - } - | - - { - parameter = new Sequence.Parameter(Sequence.ParameterType.NOMINVALUE); - sequenceParameters.add(parameter); - } - | - token= - { - parameter = new Sequence.Parameter(Sequence.ParameterType.MINVALUE); - parameter.setValue(Long.parseLong(token.image)); - sequenceParameters.add(parameter); - } - | - { sequenceParameters.add(new Sequence.Parameter(Sequence.ParameterType.NOCYCLE)); } - | - { sequenceParameters.add(new Sequence.Parameter(Sequence.ParameterType.CYCLE)); } - | - - { - parameter = new Sequence.Parameter(Sequence.ParameterType.NOCACHE); - sequenceParameters.add(parameter); - } - | - token= - { - parameter = new Sequence.Parameter(Sequence.ParameterType.CACHE); - parameter.setValue(Long.parseLong(token.image)); - sequenceParameters.add(parameter); - } - | - { sequenceParameters.add(new Sequence.Parameter(Sequence.ParameterType.ORDER)); } - | - { sequenceParameters.add(new Sequence.Parameter(Sequence.ParameterType.NOORDER)); } - | - { sequenceParameters.add(new Sequence.Parameter(Sequence.ParameterType.KEEP)); } - | - { sequenceParameters.add(new Sequence.Parameter(Sequence.ParameterType.NOKEEP)); } - | - { sequenceParameters.add(new Sequence.Parameter(Sequence.ParameterType.SESSION)); } - | - { sequenceParameters.add(new Sequence.Parameter(Sequence.ParameterType.GLOBAL)); } + LOOKAHEAD({ isKeywordAhead("FUNCTION") || isKeywordAhead("PROCEDURE") || isKeywordAhead("ROUTINE") || isKeywordAhead("AGGREGATE") }) + ( LOOKAHEAD(1) { kind = "PROCEDURE"; } | kind=RelObjectName() ) + routine=RoutineReference("AGGREGATE".equalsIgnoreCase(kind)) + { result.setKind(typeDdlEnum(ExtensionObject.Kind.class, kind)); result.setRoutine(routine); } + | LOOKAHEAD({ isKeywordAhead("CAST") }) "(" + type=ColDataType() { result.setSourceType(type); } + type=ColDataType() { result.setTargetType(type); } ")" { result.setKind(ExtensionObject.Kind.CAST); } + | LOOKAHEAD({ isKeywordAhead("OPERATOR") && !"CLASS".equalsIgnoreCase(getToken(2).image) && !"FAMILY".equalsIgnoreCase(getToken(2).image) }) + TypeDdlKeyword("OPERATOR") name=ExtensionOperatorName() "(" + type=ExtensionOperatorType() { result.setSourceType(type); } "," + type=ExtensionOperatorType() { result.setTargetType(type); } ")" + { result.setKind(ExtensionObject.Kind.OPERATOR); result.setName(name); } + | LOOKAHEAD({ isKeywordAhead("TRANSFORM") }) TypeDdlKeyword("TRANSFORM") + type=ColDataType() { result.setSourceType(type); } TypeDdlKeyword("LANGUAGE") + name=RelObjectName() { result.setLanguage(name); result.setKind(ExtensionObject.Kind.TRANSFORM); } + | + ( + + ( { kind = "FOREIGN_TABLE"; } + | TypeDdlKeyword("WRAPPER") { kind = "FOREIGN_DATA_WRAPPER"; } ) + | LOOKAHEAD(2) { kind = "MATERIALIZED_VIEW"; } + | LOOKAHEAD({ isKeywordAhead("TEXT") }) + name=RelObjectName() { kind = "TEXT_SEARCH_" + name; } + | LOOKAHEAD({ isKeywordAhead("EVENT") }) { kind = "EVENT_TRIGGER"; } + | LOOKAHEAD({ isKeywordAhead("ACCESS") }) TypeDdlKeyword("ACCESS") TypeDdlKeyword("METHOD") { kind = "ACCESS_METHOD"; } + | LOOKAHEAD({ isKeywordAhead("OPERATOR") }) TypeDdlKeyword("OPERATOR") + name=RelObjectName() { kind = "OPERATOR_" + name; } + | LOOKAHEAD({ isKeywordAhead("PROCEDURAL") }) TypeDdlKeyword("PROCEDURAL") TypeDdlKeyword("LANGUAGE") + { result.setProcedural(true); kind = "LANGUAGE"; } + | kind=RelObjectName() ) - )* + { result.setKind(typeDdlEnum(ExtensionObject.Kind.class, kind)); } + name=TypeDdlName() { result.setName(name); } + [ name=RelObjectName() { result.setAccessMethod(name); } ] + ) { - return sequenceParameters; + requireTypeDdlSyntax((result.getKind() == ExtensionObject.Kind.OPERATOR_CLASS || result.getKind() == ExtensionObject.Kind.OPERATOR_FAMILY) + == (result.getAccessMethod() != null), "USING is required only for operator classes and families"); + return result; } } -CreateSequence CreateSequence(): +AlterExtension AlterExtension(): +{ AlterExtension result = new AlterExtension(); String name; Expression version; ExtensionObject member; } +{ + TypeDdlKeyword("EXTENSION") name=RelObjectName() { result.setName(name); } + ( + { result.setAction(AlterExtension.Action.UPDATE); } + [ version=ExtensionVersion() { result.setVersion(version); } ] + | name=RelObjectName() { result.setAction(AlterExtension.Action.SET_SCHEMA); result.setSchema(name); } + | ( { result.setAction(AlterExtension.Action.ADD); } | { result.setAction(AlterExtension.Action.DROP); } ) + member=ExtensionObject() { result.setMember(member); } + ) + { return result; } +} + +void ReplicationKeyword(String expected): +{ Token token; } +{ token= { requireReplicationSyntax(expected.equalsIgnoreCase(token.image), "Expected " + expected); } } + +Expression ReplicationOptionValue(): +{ Token token; Expression result; } { - CreateSequence createSequence = new CreateSequence(); - Sequence sequence; - List sequenceParameters; - Token dataType = null; + ( LOOKAHEAD(1) token= { result = linkToLastToken(new Column(token.image)); } | result=PrimaryExpression() ) + { return result; } } + +PublicationOption PublicationOption(): +{ String name; Expression value = null; } { - sequence=Sequence() { createSequence.setSequence(sequence); } - [ ( dataType= | dataType= ) { sequence.setDataType(dataType.image); } ] - sequenceParameters = SequenceParameters() - { - sequence.setParameters(sequenceParameters); - return createSequence; - } + name=RelObjectName() [ "=" value=ReplicationOptionValue() ] + { return new PublicationOption(name, replicationEnum(PublicationOption.Kind.class, name), value); } } -AlterSequence AlterSequence(): +List PublicationOptions(): +{ List options = new ArrayList(); PublicationOption option; } { - AlterSequence alterSequence = new AlterSequence(); - Sequence sequence; - List sequenceParameters; + "(" option=PublicationOption() { options.add(option); } + ( "," option=PublicationOption() { options.add(option); } )* ")" + { return options; } } + +SubscriptionOption SubscriptionOption(): +{ String name; Expression value = null; } { - sequence=Sequence() { alterSequence.setSequence(sequence); } - sequenceParameters = SequenceParameters() - { - sequence.setParameters(sequenceParameters); - return alterSequence; - } + name=RelObjectName() [ "=" value=ReplicationOptionValue() ] + { return new SubscriptionOption(name, replicationEnum(SubscriptionOption.Kind.class, name), value); } } -Statement Create(): +List SubscriptionOptions(): +{ List options = new ArrayList(); SubscriptionOption option; } { - boolean isUsingOrReplace=false; - Token tk; - Statement statement; - List captureRest; + "(" option=SubscriptionOption() { options.add(option); } + ( "," option=SubscriptionOption() { options.add(option); } )* ")" + { return options; } } + +PublicationTable PublicationTable(boolean drop): +{ PublicationTable result = new PublicationTable(); Table table; String name; Expression expression; ExpressionList columns; } { - [ { isUsingOrReplace = true; } ] - ( - statement = CreateFunctionStatement(isUsingOrReplace) - | - statement = CreateSchema() - | - statement = CreateSequence() - | - statement = CreateSynonym(isUsingOrReplace) - | - LOOKAHEAD(3) statement = CreateTable(isUsingOrReplace) - | - LOOKAHEAD(2) statement = CreateView(isUsingOrReplace) - | - statement = CreatePolicy() - | - // @fixme: must appear with TRIGGER before INDEX or it will collide with INDEX's CreateParameter() production - ( tk= | tk= ) captureRest = captureRest() - { - statement = new UnsupportedStatement("CREATE " + tk.image, captureRest); - } - | - /* @fixme - * Create Index uses CreateParameter() which allows all kind of tokens - * it can conflict with other statements and must be at the end right now - */ - statement = CreateIndex() - | - captureRest = captureRest() - { - statement = new UnsupportedStatement("CREATE", captureRest); - } - ) + [ LOOKAHEAD(2) { result.setOnly(true); } ] + table=Table() { result.setTable(table); } + [ "*" { result.setIncludeDescendants(true); } ] + [ LOOKAHEAD(1) "(" { columns = new ExpressionList(); } + name=RelObjectName() { columns.add(linkToLastToken(createColumn(name))); } + ( "," name=RelObjectName() { columns.add(linkToLastToken(createColumn(name))); } )* ")" + { result.setColumns(linkElements(columns)); } ] + [ "(" expression=Expression() ")" { result.setWhere(expression); } ] { - return statement; + requireReplicationSyntax(!(result.isOnly() && result.isIncludeDescendants()), "ONLY and * cannot be combined"); + requireReplicationSyntax(!drop || (result.getColumns() == null && result.getWhere() == null), "DROP publication tables cannot have column lists or filters"); + return result; } } -CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace): +PublicationTarget PublicationTarget(boolean drop): +{ PublicationTarget result = new PublicationTarget(); PublicationTable table; String schema; } { - CreateFunctionalStatement type = null; - List tokens = new LinkedList(); - String statementType = null; + ( + { result.setKind(PublicationTarget.Kind.TABLE); } + table=PublicationTable(drop) { result.getTables().add(table); } + ( LOOKAHEAD({ getToken(1).image.equals(",") && getToken(2).kind != K_TABLE && getToken(2).kind != K_TABLES }) + "," table=PublicationTable(drop) { result.getTables().add(table); } )* + | + { result.setKind(PublicationTarget.Kind.TABLES_IN_SCHEMA); } + schema=RelObjectName() { result.getSchemas().add(schema); } + ( LOOKAHEAD({ getToken(1).image.equals(",") && getToken(2).kind != K_TABLE && getToken(2).kind != K_TABLES }) + "," schema=RelObjectName() { result.getSchemas().add(schema); } )* + ) + { return result; } } + +List PublicationTargets(boolean drop): +{ List targets = new ArrayList(); PublicationTarget target; } { - ( - { statementType = "FUNCTION"; } - | - { statementType = "PROCEDURE"; } - ) - tokens=captureFunctionBody() - { - if(statementType.equals("FUNCTION")) { - type = new CreateFunction(isUsingOrReplace, tokens); - } - if(statementType.equals("PROCEDURE")) { - type = new CreateProcedure(isUsingOrReplace, tokens); - } + target=PublicationTarget(drop) { targets.add(target); } + ( "," target=PublicationTarget(drop) { targets.add(target); } )* + { return targets; } +} - return type; - } +CreatePublication CreatePublication(): +{ CreatePublication result = new CreatePublication(); String name; List targets; List options; } +{ + ReplicationKeyword("PUBLICATION") name=RelObjectName() { result.setName(name); } + [ + + ( { result.setAllTables(true); } + | targets=PublicationTargets(false) { result.setTargets(targets); } ) + ] + [ LOOKAHEAD(2) options=PublicationOptions() { result.setOptions(options); } ] + { return result; } } -CreateSynonym CreateSynonym(boolean isUsingOrReplace): +AlterPublication AlterPublication(): +{ AlterPublication result = new AlterPublication(); String name; List targets; List options; boolean drop = false; } { - CreateSynonym createSynonym = new CreateSynonym(); - Synonym synonym; - boolean publicSynonym = false; - ObjectNames data = null; + ReplicationKeyword("PUBLICATION") name=RelObjectName() { result.setName(name); } + ( + LOOKAHEAD({ isKeywordAhead("OWNER") }) ReplicationKeyword("OWNER") + name=RelObjectName() { result.setNewName(name); result.setAction(AlterPublication.Action.OWNER); } + | name=RelObjectName() { result.setNewName(name); result.setAction(AlterPublication.Action.RENAME); } + | LOOKAHEAD( "(") options=PublicationOptions() { result.setOptions(options); result.setAction(AlterPublication.Action.SET_OPTIONS); } + | ( { result.setAction(AlterPublication.Action.ADD); } + | { result.setAction(AlterPublication.Action.SET); } + | { result.setAction(AlterPublication.Action.DROP); drop = true; } ) + targets=PublicationTargets(drop) { result.setTargets(targets); } + ) + { return result; } } + +List SubscriptionPublications(): +{ List names = new ArrayList(); String name; } { - [ { publicSynonym = true; } ] - synonym=Synonym() { createSynonym.setSynonym(synonym); } - data = RelObjectNames() - { - createSynonym.setOrReplace(isUsingOrReplace); - createSynonym.setPublicSynonym(publicSynonym); - createSynonym.setForList(data.getNames()); - return createSynonym; - } + name=RelObjectName() { names.add(name); } + ( "," name=RelObjectName() { names.add(name); } )* + { return names; } } -Synonym Synonym() #Synonym : +CreateSubscription CreateSubscription(): +{ CreateSubscription result = new CreateSubscription(); String name; Token connection; List publications; List options; } { - ObjectNames data = null; - String serverName = null, databaseName = null, schemaName = null, sequenceName = null; + ReplicationKeyword("SUBSCRIPTION") name=RelObjectName() { result.setName(name); } + ReplicationKeyword("CONNECTION") connection= { result.setConnection(stringValue(connection)); } + ReplicationKeyword("PUBLICATION") publications=SubscriptionPublications() { result.setPublications(publications); } + [ LOOKAHEAD(2) options=SubscriptionOptions() { result.setOptions(options); } ] + { return result; } } + +AlterSubscription AlterSubscription(): +{ AlterSubscription result = new AlterSubscription(); String name; Token connection; List publications; List options; } { - data = RelObjectNames() - { - Synonym synonym = new Synonym(data.getNames()); - linkAST(synonym,jjtThis); - return synonym; - } + ReplicationKeyword("SUBSCRIPTION") name=RelObjectName() { result.setName(name); } + ( + LOOKAHEAD({ isKeywordAhead("CONNECTION") }) ReplicationKeyword("CONNECTION") + connection= { result.setConnection(stringValue(connection)); result.setAction(AlterSubscription.Action.CONNECTION); } + | LOOKAHEAD({ isKeywordAhead("OWNER") }) ReplicationKeyword("OWNER") + name=RelObjectName() { result.setNewName(name); result.setAction(AlterSubscription.Action.OWNER); } + | name=RelObjectName() { result.setNewName(name); result.setAction(AlterSubscription.Action.RENAME); } + | { result.setAction(AlterSubscription.Action.ENABLE); } + | { result.setAction(AlterSubscription.Action.DISABLE); } + | options=SubscriptionOptions() { + requireReplicationSyntax(options.size() == 1 && options.get(0).getKind() == SubscriptionOption.Kind.LSN + && options.get(0).getValue() != null, "SKIP requires one lsn = value option"); + result.setOptions(options); result.setAction(AlterSubscription.Action.SKIP); + } + | LOOKAHEAD( "(") options=SubscriptionOptions() { result.setOptions(options); result.setAction(AlterSubscription.Action.SET_OPTIONS); } + | ReplicationKeyword("PUBLICATION") { result.setAction(AlterSubscription.Action.REFRESH_PUBLICATION); } + [ LOOKAHEAD(2) options=SubscriptionOptions() { result.setOptions(options); } ] + | ( { result.setAction(AlterSubscription.Action.SET_PUBLICATION); } + | { result.setAction(AlterSubscription.Action.ADD_PUBLICATION); } + | { result.setAction(AlterSubscription.Action.DROP_PUBLICATION); } ) + ReplicationKeyword("PUBLICATION") publications=SubscriptionPublications() { result.setPublications(publications); } + [ LOOKAHEAD(2) options=SubscriptionOptions() { result.setOptions(options); } ] + ) + { return result; } } CreatePolicy CreatePolicy() #CreatePolicy: @@ -13178,15 +25046,20 @@ CreatePolicy CreatePolicy() #CreatePolicy: CreatePolicy createPolicy = new CreatePolicy(); String policyName; Table table; + String policyMode = null; Token commandToken = null; - String roleName; - Expression usingExpr = null; - Expression checkExpr = null; + PolicyOptions options; } { policyName=RelObjectName() { createPolicy.setPolicyName(policyName); } table=Table() { createPolicy.setTable(table); } + [ + LOOKAHEAD({ isKeywordAhead("PERMISSIVE") || isKeywordAhead("RESTRICTIVE") }) + policyMode=RelObjectName() + { createPolicy.setPolicyMode(PolicyMode.from(policyMode)); } + ] + [ ( commandToken= | commandToken= @@ -13194,17 +25067,10 @@ CreatePolicy CreatePolicy() #CreatePolicy: | commandToken= | commandToken= ) - { createPolicy.setCommand(commandToken.image); } - ] - - [ - roleName=RelObjectName() { createPolicy.addRole(roleName); } - ( "," roleName=RelObjectName() { createPolicy.addRole(roleName); } )* + { createPolicy.setPolicyCommand(PolicyCommand.from(commandToken.image)); } ] - [ "(" usingExpr=Expression() ")" { createPolicy.setUsingExpression(usingExpr); } ] - - [ LOOKAHEAD(2) "(" checkExpr=Expression() ")" { createPolicy.setWithCheckExpression(checkExpr); } ] + options=PolicyOptions() { createPolicy.setOptions(options); } { @@ -13212,14 +25078,44 @@ CreatePolicy CreatePolicy() #CreatePolicy: } } + +PolicyOptions PolicyOptions(): +{ PolicyOptions options = new PolicyOptions(); String role; Expression expression; } +{ + [ role=RelObjectName() { options.getRoles().add(role); } + ( "," role=RelObjectName() { options.getRoles().add(role); } )* ] + [ "(" expression=Expression() ")" { options.setUsingExpression(expression); } ] + [ LOOKAHEAD(2) "(" expression=Expression() ")" { options.setWithCheckExpression(expression); } ] + { return options; } +} +AlterPolicy AlterPolicy(): +{ AlterPolicy statement = new AlterPolicy(); String name; Table table; PolicyOptions options; } +{ + name=RelObjectName() { statement.setPolicyName(name); } + table=Table() { statement.setTable(table); } + ( name=RelObjectName() { statement.setNewName(name); } + | options=PolicyOptions() { statement.setOptions(options); } ) + { return statement; } +} +DropPolicy DropPolicy(): +{ DropPolicy statement = new DropPolicy(); String name; Table table; Token behavior; } +{ + [ LOOKAHEAD(2) { statement.setIfExists(true); } ] + name=RelObjectName() { statement.setPolicyName(name); } + table=Table() { statement.setTable(table); } + [ (behavior= | behavior=) { statement.setBehavior(CascadeBehavior.valueOf(behavior.image.toUpperCase(Locale.ROOT))); } ] + { return statement; } +} + UnsupportedStatement UnsupportedStatement(): { List tokens = new LinkedList(); + Token first = getToken(1); } { tokens=captureUnsupportedStatementDeclaration() { - return new UnsupportedStatement(tokens); + return linked(new UnsupportedStatement(tokens), first); } } @@ -13243,38 +25139,71 @@ List captureRest() { } /** -* Reads the tokens of a function or procedure body. -* A function body can end in 2 ways: -* 1) BEGIN...END; -* 2) Postgres: $$...$$...; -*/ + * Captures opaque routine declarations through their terminating semicolon. + * PostgreSQL AS string bodies can use ordinary, escape or dollar quotes. + * The boundary tracker distinguishes simple statements, declaration sections, + * compound bodies, and expression CASE from procedural END qualifiers. + * With a list, MySQL characteristics between the header and the body are also + * parsed into it; their tokens are captured as well. + */ JAVACODE -List captureFunctionBody() { +List captureFunctionBody(List characteristics) { List tokens = new LinkedList(); Token tok; - boolean foundEnd = false; + RoutineBodyBoundary boundary = new RoutineBodyBoundary(); while(true) { tok = getToken(1); int l = tokens.size(); - if( tok.kind == EOF || ( foundEnd && tok.kind == ST_SEMICOLON) ) { + if (characteristics != null && boundary.isBeforeBody() && isMySqlCreateRoutineCharacteristicAhead()) { + characteristics.add(MySqlRoutineCharacteristic(true)); + for (Token consumed = tok; ; consumed = consumed.next) { + tokens.add(consumed.image); + boundary.accept(consumed, consumed == token ? getToken(1) : consumed.next); + if (consumed == token) { + break; + } + } + continue; + } + if (boundary.endsAt(tok)) { if (tok.kind == ST_SEMICOLON) { tokens.add(tok.image); } break; + } else if (boundary.startsQuotedBody(tok, getToken(2))) { + tokens.add(getNextToken().image); + tokens.add(captureRoutineBodyLiteral()); + boundary.quotedBody(); + continue; } else if ( l>0 && ( tok.image.equals(".") || tokens.get(l-1).endsWith(".")) ) { tokens.set(l-1, tokens.get(l-1) + tok.image); } else { tokens.add(tok.image); } - foundEnd |= (tok.kind == K_END) - || ( tok.image.trim().startsWith("$$") && tok.image.trim().endsWith("$$")) ; - + boundary.accept(tok, getToken(2)); tok = getNextToken(); } return tokens; } +/** + * Keeps the separator required by PostgreSQL's continued string literals. + * The routine's legacy token list otherwise renders every gap as one space. + */ +JAVACODE +String captureRoutineBodyLiteral() { + String literal = getNextToken().image; + if (literal.startsWith("$") || !isAdjacentStringConcat()) { + return literal; + } + StringBuilder body = new StringBuilder(literal); + while (isAdjacentStringConcat() && getToken(1).image.startsWith("'")) { + body.append('\n').append(getNextToken().image); + } + return body.toString(); +} + /** * Reads the tokens of a Postgres dollar quoted string, rebuilding the white space of the text based on each token's position and length @@ -13393,7 +25322,8 @@ TranscodingFunction TranscodingFunction() #TranscodingFunction : ( keywordToken= | keywordToken= | keywordToken= ) "(" ( - LOOKAHEAD(4) colDataType = ColDataType() + LOOKAHEAD(4, { !Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) }) + colDataType = ColDataType() "," expression = Expression() [ "," style = { transcodingName = style.image; } ] @@ -13403,11 +25333,14 @@ TranscodingFunction TranscodingFunction() #TranscodingFunction : | ( expression = Expression() - transcodingName=IdentifierChain() - - { - transcodingFunction = new TranscodingFunction(expression, transcodingName); - } + ( + transcodingName=IdentifierChain() + { transcodingFunction = new TranscodingFunction(keywordToken.image, expression, transcodingName); } + | + "," colDataType=ColDataType() + { transcodingFunction = new TranscodingFunction(keywordToken.image, colDataType, expression, null) + .setSyntax(TranscodingFunction.Syntax.TYPE_LAST); } + ) ) ) ")" @@ -13615,3 +25548,352 @@ String TimeTravelAfterAlias(): return builder.toString(); } } + +CreateStatistics CreateStatistics(): +{ + CreateStatistics statement = new CreateStatistics(); + String name; CreateStatistics.Kind kind; Expression expression; Table table; +} +{ + ContextualKeyword("STATISTICS") + [ LOOKAHEAD(2) { statement.setIfNotExists(true); } ] + [ LOOKAHEAD({ getToken(1).kind != K_ON && getToken(1).kind != OPENING_BRACKET }) + name=TypeDdlName() { statement.setName(name); } ] + [ "(" name=RelObjectName() { kind=accessEnum(CreateStatistics.Kind.class,name); statement.getKinds().add(kind); } + ( "," name=RelObjectName() { kind=accessEnum(CreateStatistics.Kind.class,name); + requireDdlSyntax(!statement.getKinds().contains(kind),"Duplicate statistics kind"); statement.getKinds().add(kind); } )* ")" ] + expression=PostgreSqlStatisticsExpression() { statement.getExpressions().add(expression); } + ( "," expression=PostgreSqlStatisticsExpression() { statement.getExpressions().add(expression); } )* + table=Table() { statement.setTable(table); } + { + requireDdlSyntax(!statement.isIfNotExists() || statement.getName()!=null,"IF NOT EXISTS requires a statistics name"); + requireDdlSyntax(statement.getExpressions().size()>1 || statement.getKinds().isEmpty() + && statement.getExpressions().get(0) instanceof ParenthesedExpressionList, + "Single-expression statistics require a parenthesized expression and no explicit kinds"); + return statement; + } +} +Expression PostgreSqlStatisticsExpression(): +{ Expression expression; String name; Token open; } +{ + ( open="(" expression=Expression() ")" { expression = new ParenthesedExpressionList(expression); linkAST((ASTNodeAccess) expression, open); } + | name=RelObjectName() { expression = linkToLastToken(createColumn(name)); } ) + { return expression; } +} +AlterStatistics AlterStatistics(): +{ AlterStatistics statement = new AlterStatistics(); String name; Long target; } +{ + ContextualKeyword("STATISTICS") name=TypeDdlName() { statement.setName(name); } + ( name=RelObjectName() { statement.setAction(AlterStatistics.Action.RENAME); statement.setNewName(name); } + | ContextualKeyword("OWNER") name=RelObjectName() { statement.setAction(AlterStatistics.Action.OWNER); statement.setOwner(name); } + | ( name=RelObjectName() { statement.setAction(AlterStatistics.Action.SET_SCHEMA); statement.setSchemaName(name); } + | ContextualKeyword("STATISTICS") { statement.setAction(AlterStatistics.Action.SET_STATISTICS); } + ( | target=SequenceParameterValue() { + requireDdlSyntax(target>=-1 && target<=10000,"Statistics target must be DEFAULT or between -1 and 10000"); + statement.setStatistics(target.intValue()); } ) ) ) + { return statement; } +} + +CreateForeignDataWrapper CreateForeignDataWrapper(): +{ + CreateForeignDataWrapper statement = new CreateForeignDataWrapper(); + String name; ForeignDataWrapperFunctions functions; List options; +} +{ + ContextualKeyword("WRAPPER") name=RelObjectName() + functions=ForeignDataWrapperFunctions() + { statement.setName(name); statement.setFunctions(functions); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(false) { statement.setOptions(options); } ] + { return statement; } +} + +AlterForeignDataWrapper AlterForeignDataWrapper(): +{ + AlterForeignDataWrapper statement = new AlterForeignDataWrapper(); + String name; ForeignDataWrapperFunctions functions; List options; +} +{ + ContextualKeyword("WRAPPER") name=RelObjectName() { statement.setName(name); } + ( + name=RelObjectName() + { statement.setAction(ForeignObjectAlterAction.RENAME); statement.setNewName(name); } + | LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(ForeignObjectAlterAction.OWNER); statement.setOwner(name); } + | functions=ForeignDataWrapperFunctions() { statement.setFunctions(functions); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(true) { statement.setOptions(options); } ] + { requireDdlSyntax(functions.isHandlerSpecified() || functions.isValidatorSpecified() + || statement.getOptions() != null, "ALTER FOREIGN DATA WRAPPER requires an action"); } + ) + { return statement; } +} + +ForeignDataWrapperFunctions ForeignDataWrapperFunctions(): +{ + ForeignDataWrapperFunctions functions = new ForeignDataWrapperFunctions(); + String name; boolean handler; +} +{ + ( LOOKAHEAD({ isKeywordAhead("HANDLER") || isKeywordAhead("VALIDATOR") || getToken(1).kind == K_NO }) + { name = null; } + ( + + ( LOOKAHEAD({ isKeywordAhead("HANDLER") }) ContextualKeyword("HANDLER") { handler = true; } + | ContextualKeyword("VALIDATOR") { handler = false; } ) + | + ( LOOKAHEAD({ isKeywordAhead("HANDLER") }) ContextualKeyword("HANDLER") { handler = true; } + | ContextualKeyword("VALIDATOR") { handler = false; } ) + name=AccessQualifiedName() + ) + { + if (handler) { + requireDdlSyntax(!functions.isHandlerSpecified(), "Duplicate HANDLER clause"); functions.setHandler(name); + } else { + requireDdlSyntax(!functions.isValidatorSpecified(), "Duplicate VALIDATOR clause"); functions.setValidator(name); + } + } + )* + { return functions; } +} + +CreateServer CreateServer(): +{ + CreateServer statement = new CreateServer(); String name; Token type; + Expression version; List options; +} +{ + ContextualKeyword("SERVER") + [ LOOKAHEAD(2) { statement.setIfNotExists(true); } ] + name=RelObjectName() { statement.setName(name); } + [ type= { statement.setType(stringValue(type)); } ] + [ version=ForeignServerVersion() { statement.setVersion(version); } ] + ContextualKeyword("WRAPPER") name=RelObjectName() { statement.setForeignDataWrapper(name); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(false) { statement.setOptions(options); } ] + { return statement; } +} + +Expression ForeignServerVersion(): +{ Token value; Expression version; } +{ + ( value= { version = stringValue(value); } + | { version = new NullValue(); } ) + { return version; } +} + +AlterServer AlterServer(): +{ + AlterServer statement = new AlterServer(); String name; + Expression version; List options; +} +{ + ContextualKeyword("SERVER") name=RelObjectName() { statement.setName(name); } + ( + name=RelObjectName() + { statement.setAction(ForeignObjectAlterAction.RENAME); statement.setNewName(name); } + | LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(ForeignObjectAlterAction.OWNER); statement.setOwner(name); } + | [ version=ForeignServerVersion() { statement.setVersion(version); } ] + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(true) { statement.setOptions(options); } ] + { requireDdlSyntax(statement.getVersion() != null || statement.getOptions() != null, + "ALTER SERVER requires VERSION or OPTIONS"); } + ) + { return statement; } +} + +CreateUserMapping CreateUserMapping(): +{ + CreateUserMapping statement = new CreateUserMapping(); String name; List options; +} +{ + ContextualKeyword("MAPPING") + [ LOOKAHEAD(2) { statement.setIfNotExists(true); } ] + name=RelObjectName() { statement.setUser(name); } + ContextualKeyword("SERVER") name=RelObjectName() { statement.setServer(name); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(false) { statement.setOptions(options); } ] + { return statement; } +} + +DropUserMapping DropUserMapping(): +{ + DropUserMapping statement = new DropUserMapping(); String name; +} +{ + ContextualKeyword("MAPPING") + [ LOOKAHEAD(2) { statement.setIfExists(true); } ] + name=RelObjectName() { statement.setUser(name); } + ContextualKeyword("SERVER") name=RelObjectName() { statement.setServer(name); } + { return statement; } +} + +AlterUserMapping AlterUserMapping(): +{ + AlterUserMapping statement = new AlterUserMapping(); String name; List options; +} +{ + ContextualKeyword("MAPPING") name=RelObjectName() { statement.setUser(name); } + ContextualKeyword("SERVER") name=RelObjectName() { statement.setServer(name); } + options=ForeignDataOptions(true) { statement.setOptions(options); return statement; } +} + +CreateAccessMethod CreateAccessMethod(): +{ + CreateAccessMethod statement = new CreateAccessMethod(); + String name; +} +{ + ContextualKeyword("ACCESS") ContextualKeyword("METHOD") name=RelObjectName() + { statement.setName(name); } + ( { statement.setType(CreateAccessMethod.Type.TABLE); } + | { statement.setType(CreateAccessMethod.Type.INDEX); } ) + ContextualKeyword("HANDLER") name=AccessQualifiedName() + { statement.setHandler(name); return statement; } +} + +CreateTextSearchConfiguration CreateTextSearchConfiguration(): +{ CreateTextSearchConfiguration statement = new CreateTextSearchConfiguration(); String name; } +{ + ContextualKeyword("CONFIGURATION") + name=AccessQualifiedName() { statement.setName(name); } + "(" ( { statement.setSourceKind(CreateTextSearchConfiguration.SourceKind.PARSER); } + | { statement.setSourceKind(CreateTextSearchConfiguration.SourceKind.COPY); } ) + "=" name=TextSearchConfigurationSource() ")" { statement.setSourceName(name); return statement; } +} + +AlterTextSearchConfiguration AlterTextSearchConfiguration(): +{ + AlterTextSearchConfiguration statement = new AlterTextSearchConfiguration(); + String name; List names; +} +{ + ContextualKeyword("CONFIGURATION") + name=AccessQualifiedName() { statement.setName(name); } + ( + ContextualKeyword("MAPPING") names=AccessNames() { statement.setTokenTypes(names); } + names=TextSearchDictionaryNames() + { statement.setAction(AlterTextSearchConfiguration.Action.ADD_MAPPING); statement.setDictionaries(names); } + | + ContextualKeyword("MAPPING") + [ names=AccessNames() { statement.setTokenTypes(names); } ] + ( name=AccessQualifiedName() { statement.setOldDictionary(name); } + name=AccessQualifiedName() + { statement.setAction(AlterTextSearchConfiguration.Action.REPLACE_MAPPING); statement.setNewDictionary(name); } + | names=TextSearchDictionaryNames() { + requireDdlSyntax(!statement.getTokenTypes().isEmpty(), "ALTER MAPPING WITH requires token types"); + statement.setAction(AlterTextSearchConfiguration.Action.ALTER_MAPPING); statement.setDictionaries(names); + } ) + | + ContextualKeyword("MAPPING") + [ LOOKAHEAD(2) { statement.setIfExists(true); } ] + names=AccessNames() + { statement.setAction(AlterTextSearchConfiguration.Action.DROP_MAPPING); statement.setTokenTypes(names); } + | + name=RelObjectName() + { statement.setAction(AlterTextSearchConfiguration.Action.RENAME); statement.setNewName(name); } + | + name=RelObjectName() + { statement.setAction(AlterTextSearchConfiguration.Action.SET_SCHEMA); statement.setSchemaName(name); } + | + ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterTextSearchConfiguration.Action.OWNER); statement.setOwner(name); } + ) + { return statement; } +} + +List TextSearchDictionaryNames(): +{ List names = new ArrayList(); String name; } +{ + name=AccessQualifiedName() { names.add(name); } + ( "," name=AccessQualifiedName() { names.add(name); } )* + { return names; } +} + +String TextSearchConfigurationSource(): +{ String name; Token value; } +{ + ( value= { name = value.image; } | name=AccessQualifiedName() ) + { return name; } +} + +CreateRule CreateRule(boolean orReplace): +{ + CreateRule statement = new CreateRule(); String name; Table table; + Token event; Expression condition; Statement action; +} +{ + ContextualKeyword("RULE") name=RelObjectName() + { statement.setName(name); statement.setOrReplace(orReplace); } + ( event= | event= | event= | event= ) + { statement.setEvent(CreateRule.Event.valueOf(event.image.toUpperCase(java.util.Locale.ROOT))); } + table=Table() { statement.setTable(table); } + [ condition=Expression() { statement.setWhereExpression(condition); } ] + + [ LOOKAHEAD({ isKeywordAhead("ALSO") || isKeywordAhead("INSTEAD") }) + ( LOOKAHEAD({ isKeywordAhead("ALSO") }) ContextualKeyword("ALSO") { statement.setBehavior(CreateRule.Behavior.ALSO); } + | AccessKeyword("INSTEAD") { statement.setBehavior(CreateRule.Behavior.INSTEAD); } ) ] + ( + { statement.setNothing(true); } + | LOOKAHEAD(Select()) action=Select() { statement.getActions().add(action); } + | LOOKAHEAD(1, "(") "(" { statement.setUseParentheses(true); } + [ LOOKAHEAD({ getToken(1).kind != ST_SEMICOLON && !")".equals(getToken(1).image) }) + action=PostgreSqlRuleAction() { statement.getActions().add(action); } ] + ( + [ LOOKAHEAD({ getToken(1).kind != ST_SEMICOLON && !")".equals(getToken(1).image) }) + action=PostgreSqlRuleAction() { statement.getActions().add(action); } ] )* + ")" + | action=PostgreSqlRuleAction() { statement.getActions().add(action); } + ) + { return statement; } +} + +Statement PostgreSqlRuleAction(): +{ Statement statement; } +{ + statement=SingleStatement() + { + requireDdlSyntax(statement instanceof Select || statement instanceof TableStatement || statement instanceof Insert + || statement instanceof Update || statement instanceof Delete + || statement instanceof NotifyStatement, + "A rule action must be SELECT, INSERT, UPDATE, DELETE or NOTIFY"); + return statement; + } +} + +NotifyStatement NotifyStatement(): +{ NotifyStatement statement = new NotifyStatement(); String channel; Token payload; } +{ + channel=RelObjectName() { statement.setChannel(channel); } + [ "," payload= { statement.setPayload(stringValue(payload)); } ] + { return statement; } +} + +CreateCollation CreateCollation(): +{ + CreateCollation statement = new CreateCollation(); String name; List options; +} +{ + ContextualKeyword("COLLATION") + [ LOOKAHEAD(2) { statement.setIfNotExists(true); } ] + name=AccessQualifiedName() { statement.setName(name); } + ( name=AccessQualifiedName() { statement.setSourceCollation(name); } + | options=PostgreSqlOptions(true) { + for (IndexOption option : options) { + requireDdlSyntax(option.getValue() == null || option.isUseEquals(), + "Collation parameter values require an equals sign"); + } + statement.setOptions(options); + } ) + { return statement; } +} + +AlterCollation AlterCollation(): +{ AlterCollation statement = new AlterCollation(); String name; } +{ + ContextualKeyword("COLLATION") name=AccessQualifiedName() { statement.setName(name); } + ( { statement.setAction(AlterCollation.Action.REFRESH_VERSION); } + | name=RelObjectName() + { statement.setAction(AlterCollation.Action.RENAME); statement.setNewName(name); } + | name=RelObjectName() + { statement.setAction(AlterCollation.Action.SET_SCHEMA); statement.setSchemaName(name); } + | ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterCollation.Action.OWNER); statement.setOwner(name); } ) + { return statement; } +} diff --git a/src/site/sphinx/contribution.rst b/src/site/sphinx/contribution.rst index 0114af33dd..8510822e06 100644 --- a/src/site/sphinx/contribution.rst +++ b/src/site/sphinx/contribution.rst @@ -87,10 +87,12 @@ The JSQLParser is generated by ``JavaCC`` based on the provided Grammar. The Gra .. code-block:: text :caption: JMH performance results - Benchmark (version) Mode Cnt Score Error Units - JSQLParserBenchmark.parseSQLStatements latest avgt 30 83.504 ± 1.557 ms/op - JSQLParserBenchmark.parseSQLStatements 5.2 avgt 30 400.876 ± 8.291 ms/op - JSQLParserBenchmark.parseSQLStatements 5.1 avgt 30 85.731 ± 1.288 ms/op + Benchmark (version) Mode Cnt Score Error Units + JSQLParserBenchmark.parseSQLStatements latest avgt 15 21.138 ± 0.557 ms/op + JSQLParserBenchmark.parseSQLStatements 5.4 avgt 15 32.528 ± 0.666 ms/op + JSQLParserBenchmark.parseSQLStatements 5.3 avgt 15 983.459 ± 8.197 ms/op + JSQLParserBenchmark.parseSQLStatements 5.1 avgt 15 319.601 ± 4.081 ms/op + 8) Create your `GitHub Pull Request `_ @@ -174,4 +176,4 @@ Please consider using `Conventional Commits` and structure your commit message a * - **revert** - reverts one or many previous commits -Please visit `Better Programming `_ for more information and guidance. \ No newline at end of file +Please visit `Better Programming `_ for more information and guidance. diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index 9c5b6fbf77..a5a6969f6a 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -22,104 +22,245 @@ Java SQL Parser Library keywords changelog -.. image:: https://img.shields.io/github/release/JSQLParser/JSqlParser?include_prereleases=&sort=semver&color=blue - :alt: GitGub Release Badge - :target: https://github.com/JSQLParser/JSqlParser/releases - -.. image:: https://img.shields.io/github/issues/JSQLParser/JSqlParser - :alt: GitGub Issues Badge - :target: https://github.com/JSQLParser/JSqlParser/issues - -.. image:: https://badgen.net/maven/v/maven-central/com.github.jsqlparser/jsqlparser - :alt: Maven Badge - :target: https://mvnrepository.com/artifact/com.github.jsqlparser/jsqlparser - .. image:: https://github.com/JSQLParser/JSqlParser/actions/workflows/ci.yml/badge.svg :alt: CI Status :target: https://github.com/JSQLParser/JSqlParser/actions/workflows/ci.yml .. image:: https://coveralls.io/repos/JSQLParser/JSqlParser/badge.svg?branch=master :alt: Coverage Status - :target: https://coveralls.io/github/com.github.jsqlparser/jsqlparser?branch=master + :target: https://coveralls.io/r/JSQLParser/JSqlParser?branch=master .. image:: https://app.codacy.com/project/badge/Grade/6f9a2d7eb98f45969749e101322634a1 :alt: Codacy Status - :target: https://app.codacy.com/gh/com.github.jsqlparser/jsqlparser/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade + :target: https://www.codacy.com/gh/JSQLParser/JSqlParser/dashboard + +.. image:: https://img.shields.io/maven-central/v/com.manticore-projects.jsqlformatter/jsqlparser.svg?label=manticore%20build&color=ff420e + :alt: Manticore Build + :target: https://central.sonatype.com/artifact/com.manticore-projects.jsqlformatter/jsqlparser + +.. image:: https://img.shields.io/maven-central/v/com.github.jsqlparser/jsqlparser.svg?label=upstream%20release + :alt: Maven Central + :target: https://central.sonatype.com/artifact/com.github.jsqlparser/jsqlparser .. image:: https://www.javadoc.io/badge/com.github.jsqlparser/jsqlparser.svg :alt: Java Docs :target: https://javadoc.io/doc/com.github.jsqlparser/jsqlparser/latest/index.html +.. image:: https://img.shields.io/github/issues/JSQLParser/JSqlParser + :alt: GitHub Issues Badge + :target: https://github.com/JSQLParser/JSqlParser/issues -A huge thank you to our sponsor, `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! +.. image:: https://img.shields.io/github/stars/JSQLParser/JSqlParser?style=flat&label=stars + :alt: GitHub Stars + :target: https://github.com/JSQLParser/JSqlParser/stargazers .. sidebar:: Java API Website .. image:: _images/JavaAST.png -**JSQLParser** is a SQL statement parser built from JavaCC. It translates SQLs in a traversable hierarchy of Java classes. -Since the 5.0 release JSQLParser depends on Java 11 and has introduced new Visitors. Please see the :ref:`Migration to 5.0` guide. +**Turn any SQL statement into a traversable tree of Java objects — and back again.** + +**JSQLParser** is an RDBMS-agnostic SQL parser for the JVM, built with JavaCC: one grammar, all major dialects, no native extensions. Give it SQL, get an AST you can walk, rewrite and print back out. Latest stable release: |JSQLPARSER_STABLE_VERSION_LINK| Development version: |JSQLPARSER_SNAPSHOT_VERSION_LINK| +.. note:: + + Since the 5.0 release JSQLParser requires Java 11 and has introduced new Visitors. Please see the :ref:`Migration to 5.0` guide. + + +****************************** +What it does +****************************** + +.. code-block:: sql + SELECT 1 FROM dual WHERE a = b +.. raw:: html -.. code-block:: SQL - :caption: Sample SQL Statement +
+
+    SQL Text
+     └─Statements: statement.select.PlainSelect
+        ├─selectItems: statement.select.SelectItem
+        │  └─LongValue: 1
+        ├─Table: dual
+        └─where: expression.operators.relational.EqualsTo
+           ├─Column: a
+           └─Column: b
+    
+
- SELECT /*+ PARALLEL */ - cfe.id_collateral_ref.nextval - , id_collateral - FROM ( SELECT DISTINCT - a.id_collateral - FROM cfe.collateral a - LEFT JOIN cfe.collateral_ref b - ON a.id_collateral = b.id_collateral - WHERE b.id_collateral_ref IS NULL ) - ; +.. code-block:: java + + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("select 1 from dual where a=b"); + + Table table = (Table) select.getFromItem(); + Assertions.assertEquals("dual", table.getName()); + +The tree is traversable with the Visitor pattern, and the same object model works in reverse: build statements from Java with a fluent API and render them as SQL text. See :doc:`usage`. + + +****************************** +Install +****************************** + +Use the **Manticore builds**. They are immutable, versioned releases cut continuously from the current development line, and carry the grammar and performance work described below. The upstream ``com.github.jsqlparser`` release is considerably older. + +.. code-block:: xml + + + com.manticore-projects.jsqlformatter + jsqlparser + [5.3.218,) + + +Upstream coordinates, snapshots and Gradle are on the :ref:`Add JSQLParser to your Project` page. ****************************** -SQL Dialects +Performance ****************************** -**JSqlParser** is RDBMS agnostic and provides support for many dialects such as: +**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. + +.. code-block:: text -* Oracle Database -* MS SqlServer -* MySQL and MariaDB -* PostgreSQL -* H2 -* DuckDB -* Google BigQuery -* Amazon Redshift -* DataBricks -* Snowflake + Benchmark (version) Mode Cnt Score Error Units + JSQLParserBenchmark.parseSQLStatements latest avgt 15 7.602 ± 0.135 ms/op + JSQLParserBenchmark.parseSQLStatements 5.3 avgt 15 84.687 ± 3.321 ms/op -******************************* -Features -******************************* +Methodology and the full cross-parser comparison against SQLGlot, ``sqlglot[c]`` and polyglot-sql: `jsqlparser-bench `_. -* Comprehensive support for statements: - - QUERY: ``SELECT ...`` - - DML: ``INSERT ... INTO ...`` ``UPDATE ...`` ``MERGE ... INTO ...`` ``DELETE ... FROM ...`` - - DDL: ``CREATE ...`` ``ALTER ...`` ``DROP ...`` -* Nested Expressions (e.g. Sub-Selects) -* ``WITH`` clauses -* Old Oracle ``JOIN (+)`` -* PostgreSQL implicit ``CAST ::`` -* SQL Parameters (e.g. ``?`` or ``:parameter``) -* Arrays vs. T-SQL Squared Bracket Quotes -* Fluent API to create SQL Statements from java Code -* Statement De-Parser to write SQL from Java Objects -* Piped SQL (also known as FROM SQL) +****************************** +What it parses +****************************** +One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets added on demand — `open an issue `_. + +``BigQuery`` · ``Snowflake`` · ``DuckDB`` · ``Redshift`` · ``Oracle`` · ``MS SQL Server`` · ``Sybase`` · ``PostgreSQL`` · ``MySQL`` · ``MariaDB`` · ``DB2`` · ``H2`` · ``HSQLDB`` · ``Derby`` · ``SQLite`` + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - + - Statements + * - **Queries** + - ``SELECT`` · ``WITH …`` · Piped SQL + * - **DML** + - ``INSERT`` · ``UPDATE`` · ``UPSERT`` · ``MERGE`` · ``DELETE`` · ``TRUNCATE TABLE`` + * - **DDL** + - ``CREATE …`` · ``ALTER …`` · ``DROP …`` + * - **PostgreSQL ownership** + - ``DROP OWNED`` · ``REASSIGN OWNED`` + * - **MySQL table maintenance** + - ``ANALYZE`` · ``CHECK`` · ``CHECKSUM`` · ``OPTIMIZE`` · ``REPAIR TABLE`` + * - **MySQL account management** + - ``CREATE``/``ALTER``/``RENAME USER`` · ``CREATE ROLE`` · ``SET ROLE`` · ``SET DEFAULT ROLE`` · ``SET PASSWORD`` + * - **Tablespaces** + - MySQL and PostgreSQL ``CREATE TABLESPACE`` · ``ALTER TABLESPACE`` + * - **PostgreSQL RLS** + - ``CREATE POLICY`` · ``ALTER TABLE … ENABLE``/``DISABLE``/``FORCE``/``NO FORCE ROW LEVEL SECURITY`` + * - **Routine changes** + - ``ALTER FUNCTION`` · ``ALTER PROCEDURE`` · ``ALTER ROUTINE`` · PostgreSQL ``ALTER TRIGGER`` + * - **MySQL metadata statements** + - ``SHOW [FULL] COLUMNS``/``FIELDS`` · ``SHOW TABLE STATUS`` · ``SHOW CREATE TABLE``/``VIEW``/``PROCEDURE``/``FUNCTION``/``TRIGGER``/``EVENT``/``DATABASE``/``USER`` + * - **PostgreSQL administration** + - ``CREATE``/``ALTER EVENT TRIGGER`` · ``REINDEX`` · ``SECURITY LABEL`` + * - **Salesforce SOQL** + - ``INCLUDES`` · ``EXCLUDES`` + +Beyond statement shapes: nested sub-selects, bind parameters (``?``, ``:name``), window and analytic functions, Oracle hints, the old Oracle ``JOIN (+)``, PostgreSQL implicit ``CAST ::``, and the T-SQL square-bracket versus array-literal ambiguity. The complete reference is on the :doc:`syntax_stable` page; the gaps are on :doc:`unsupported`. +****************************** +Statement classification +****************************** +Any parsed statement can say what it actually does — no second parse, no visitor to write: +.. code-block:: java + + StatementFeatures features = CCJSqlParserUtil.parse(sqlStr).getFeatures(); + + // safeguard a read-only client before anything reaches the database + if (connection.isReadOnly() && features.mayModifyData()) { + throw new SQLException("rejected: " + features.getUnresolvedReferences()); + } + + // dispatch correctly + if (features.returnsResultSet()) { statement.executeQuery(sqlStr); } + else { statement.executeUpdate(sqlStr); } + +This is not ``sqlStr.startsWith("SELECT")`` with extra steps: ``RETURNING`` turns DML into a row source, a data-modifying CTE hides a ``DELETE`` inside a ``SELECT``, and ``INSERT INTO x SELECT ..`` contains a query but returns nothing. See :ref:`Classify a Statement`. + + +****************************** +Piped SQL +****************************** + +Support is progressing for Piped SQL, which writes queries in the order they actually execute rather than the order SQL historically demanded. + +.. code-block:: 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 `_, `BigQuery pipe syntax `_ and `DuckDB FROM-first syntax `_. + + +****************************** +Java version +****************************** + +.. list-table:: + :header-rows: 1 + :widths: 25 20 55 + + * - 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 :ref:`Migration to 5.0` + * - 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** + + +****************************** +Sister projects +****************************** + +- `JSQLFormatter `_ — pretty-printing and formatting of SQL text +- `JSQLTranspiler `_ — dialect-specific rewriting, column resolution and lineage, by `Starlake.ai `_ + + +****************************** +Sponsor +****************************** + +A huge thank you to our sponsor, `Starlake.ai `_, who simplify data ingestion, transformation and orchestration, enabling faster delivery of high-quality data. Starlake has been instrumental in providing Piped SQL support and numerous test cases for BigQuery, Redshift, Databricks and DuckDB. Show your support for ongoing development by visiting Starlake.ai and giving them a star. + + +****************************** +License +****************************** +Dual licensed under **LGPL 2.1** or the **Apache License, Version 2.0**. Take your pick. \ No newline at end of file diff --git a/src/site/sphinx/keywords.rst b/src/site/sphinx/keywords.rst index a9058fbaad..4fb9318da6 100644 --- a/src/site/sphinx/keywords.rst +++ b/src/site/sphinx/keywords.rst @@ -7,8 +7,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | **Keyword** | +---------------------------+ -| ABSENT | -+---------------------------+ | ALL | +---------------------------+ | AND | @@ -37,34 +35,20 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | CROSS | +---------------------------+ -| CSV | -+---------------------------+ -| CURRENT | -+---------------------------+ | DISTINCT | +---------------------------+ | DISTINCTROW | +---------------------------+ | ELSE | +---------------------------+ -| ERRORS | -+---------------------------+ | EXCEPT | +---------------------------+ | EXCLUDES | +---------------------------+ -| EXISTS | -+---------------------------+ -| EXTEND | -+---------------------------+ | FALSE | +---------------------------+ -| FBV | -+---------------------------+ | FETCH | +---------------------------+ -| FILE | -+---------------------------+ | FINAL | +---------------------------+ | FOR | @@ -89,8 +73,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | ILIKE | +---------------------------+ -| IMPORT | -+---------------------------+ | IN | +---------------------------+ | INCLUDES | @@ -145,10 +127,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | PREFERRING | +---------------------------+ -| PREWHERE | -+---------------------------+ -| PRIOR | -+---------------------------+ | PROCEDURE | +---------------------------+ | PUBLIC | @@ -157,30 +135,18 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | RETURNING | +---------------------------+ -| RETURNS | -+---------------------------+ | RIGHT | +---------------------------+ -| SAMPLE | -+---------------------------+ -| SCRIPT | -+---------------------------+ | SET | +---------------------------+ -| SETTINGS | -+---------------------------+ | SOME | +---------------------------+ | SQL_CACHE | +---------------------------+ | SQL_CALC_FOUND_ROWS | +---------------------------+ -| SQL_NO_CACHE | -+---------------------------+ | START | +---------------------------+ -| STATEMENT | -+---------------------------+ | STRAIGHT_JOIN | +---------------------------+ | TABLES | @@ -193,14 +159,10 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | TRUE | +---------------------------+ -| UNBOUNDED | -+---------------------------+ | UNION | +---------------------------+ | UNIQUE | +---------------------------+ -| UNKNOWN | -+---------------------------+ | UNPIVOT | +---------------------------+ | USE | @@ -211,8 +173,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | VALUES | +---------------------------+ -| VERIFY | -+---------------------------+ | WHEN | +---------------------------+ | WHERE | @@ -221,7 +181,5 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | WITH | +---------------------------+ -| XMLSERIALIZE | -+---------------------------+ | XOR | +---------------------------+ diff --git a/src/site/sphinx/syntax_snapshot.rst b/src/site/sphinx/syntax_snapshot.rst deleted file mode 100644 index 76af0c6bf8..0000000000 --- a/src/site/sphinx/syntax_snapshot.rst +++ /dev/null @@ -1,20923 +0,0 @@ - -********************************************************************* -SQL Syntax |JSQLPARSER_SNAPSHOT_VERSION| -********************************************************************* - -The EBNF and Railroad Diagrams for |JSQLPARSER_SNAPSHOT_VERSION|. - - -====================================================================================================================== -NonReservedWord -====================================================================================================================== - - -.. raw:: html - - - - - - ACTION - - ACTIVE - - ADD - - ADVANCE - - ADVISE - - AGAINST - - AGGREGATE - - ALGORITHM - - ALIGN - - ALTER - - ALWAYS - - ANALYZE - - APPEND_ONLY - - APPLY - - APPROXIMATE - - ARCHIVE - - ARRAY - - ASYMMETRIC - - AT - - ASC - - AUTHORIZATION - - AUTO - - AUTO_INCREMENT - - AZURE - - BASE64 - - BEFORE - - BEGIN - - BERNOULLI - - BINARY - - BIT - - BLOBSTORAGE - - BLOCK - - BOOLEAN - - BREADTH - - BRANCH - - BROWSE - - BY - - BYTES - - CACHE - - BUFFERS - - BYTE - - CALL - - CASCADE - - CASE - - CAST - - CERTIFICATE - - CHARACTER - - CHANGE - - CHANGES - - CHECKPOINT - - CHAR - - CLOSE - - CLOUD - - COALESCE - - COLLATE - - COLUMN - - COLUMNS - - COMMIT - - COMMENT - - COMMENTS - - CONFLICT - - CONSTRAINTS - - CONVERT - - CORRESPONDING - - COSTS - - COUNT - - CREATED - - CYCLE - - DATABASE - - DATA - - DECLARE - - DBA_RECYCLEBIN - - DEFAULTS - - DEPTH - - DEFERRABLE - - DELAYED - - DELETE - - DELIMIT - - DELIMITER - - DESC - - DESCRIBE - - DISABLE - - DISCARD - - DISCONNECT - - DIV - - DDL - - DML - - DO - - DOMAIN - - DRIVER - - DROP - - DUMP - - DUPLICATE - - ELEMENTS - - EMIT - - ENABLE - - ENCODING - - ENCRYPTION - - END - - ENFORCED - - ENGINE - - ERROR - - ESCAPE - - EXA - - EXCHANGE - - EXCLUDE - - EXCLUDING - - EXCLUSIVE - - EXEC - - EXECUTE - - EXPLAIN - - EXPLICIT - - EXTENDED - - EXTRACT - - EXPORT - - K_ISOLATION - FILTER - - FIRST - - FLUSH - - FOLLOWING - - FORMAT - - FULLTEXT - - FUNCTION - - GRANT - - GROUP_CONCAT - - GUARD - - HASH - - HIGH - - HIGH_PRIORITY - - HISTORY - - HOPPING - - IDENTIFIED - - IDENTITY - - INCLUDE - - INCLUDE_NULL_VALUES - - INCLUDING - - INCREMENT - - INDEX - - INFORMATION - - INSERT - - INTERLEAVE - - INTERPRET - - INVALIDATE - - INVERSE - - INVISIBLE - - ISNULL - - JDBC - - JSON - - JSON_OBJECT - - JSON_OBJECTAGG - - JSON_ARRAY - - JSON_ARRAYAGG - - KEEP - - KEY_BLOCK_SIZE - - KEY - - KEYS - - KILL - - FN - - LAST - - LEADING - - LESS - - LEVEL - - LOCAL - - LOCK - - LOCKED - - LINK - - LOG - - LOOP - - LOW - - LOW_PRIORITY - - LTRIM - - MATCH - - MATCH_ANY - - MATCH_ALL - - MATCH_PHRASE - - MATCH_PHRASE_PREFIX - - MATCH_REGEXP - - MATCHED - - MATERIALIZED - - MAX - - MAXVALUE - - MEMBER - - MERGE - - MIN - - MINVALUE - - MODE - - MODIFY - - MOVEMENT - - NAMES - - NAME - - NEVER - - NEXT - - K_NEXTVAL - NO - - NOCACHE - - NOKEEP - - NOLOCK - - NOMAXVALUE - - NOMINVALUE - - NONE - - NOORDER - - NOTHING - - NOTNULL - - NOVALIDATE - - NULLS - - NOWAIT - - OF - - OFF - - OPEN - - ORA - - ORDINALITY - - OVER - - OVERFLOW - - OVERLAPS - - OVERRIDING - - OVERWRITE - - PADDING - - PARALLEL - - PARENT - - PARSER - - PARTITION - - PARTITIONING - - PATH - - PERCENT - - PLACING - - PLAN - - PLUS - - PRECEDING - - PRIMARY - - POLICY - - PURGE - - QUERY - - QUICK - - QUIESCE - - RANGE - - RAW - - READ - - REBUILD - - RECYCLEBIN - - RECURSIVE - - REFERENCES - - REFRESH - - REGEXP - - REJECT - - RESPECT - - RLIKE - - REGEXP_LIKE - - REGISTER - - REMOTE - - REMOVE - - RENAME - - REORGANIZE - - REPAIR - - REPEATABLE - - REPLACE - - RESET - - RESTART - - RESUMABLE - - RESUME - - RESTRICT - - RESTRICTED - - RETURN - - ROLLBACK - - ROLLUP - - ROOT - - ROW - - ROWS - - RTRIM - - SAFE_CAST - - SAFE_CONVERT - - SAVEPOINT - - SCHEMA - - SEARCH - - SECURE - - SECURITY - - SEED - - SEQUENCE - - SEPARATOR - - SESSION - - SETS - - SHOW - - SHUTDOWN - - SHARE - - SIBLINGS - - SIMILAR - - SIZE - - SKIP - - SPATIAL - - STORED - - STREAM - - STRICT - - STRING - - STRUCT - - SUMMARIZE - - SUSPEND - - SWITCH - - SYMMETRIC - - SYNONYM - - SYSTEM - - SYSTEM_TIME - - SYSTEM_TIMESTAMP - - SYSTEM_VERSION - - TABLE - - TABLESPACE - - TRIGGER - - THEN - - TEMP - - K_TEXT_LITERAL - TEMPORARY - - THAN - - K_TIME_KEY_EXPR - TIMEOUT - - TO - - TRIM - - TRUNCATE - - TRY_CAST - - TRY_CONVERT - - TUMBLING - - TYPE - - UNLIMITED - - UNLOGGED - - UPDATE - - UPSERT - - UNQIESCE - - USER - - SIGNED - - K_STRING_FUNCTION_NAME - UNSIGNED - - VALIDATE - - VALIDATION - - VERBOSE - - VERSION - - VIEW - - VISIBLE - - VOLATILE - - CONCURRENTLY - - WAIT - - WITH TIES - - WITHIN - - WITHOUT - - WITHOUT_ARRAY_WRAPPER - - WORK - - XML - - XMLAGG - - XMLDATA - - XMLSCHEMA - - XMLTEXT - - XSINIL - - YAML - - YES - - ZONE - - -
- - -
         ::= 'ACTION'
-
           | 'ACTIVE'
-
           | 'ADD'
-
           | 'ADVANCE'
-
           | 'ADVISE'
-
           | 'AGAINST'
-
           | 'AGGREGATE'
-
           | 'ALGORITHM'
-
           | 'ALIGN'
-
           | 'ALTER'
-
           | 'ALWAYS'
-
           | 'ANALYZE'
-
           | 'APPEND_ONLY'
-
           | 'APPLY'
-
           | 'APPROXIMATE'
-
           | 'ARCHIVE'
-
           | 'ARRAY'
-
           | 'ASYMMETRIC'
-
           | 'AT'
-
           | 'ASC'
-
           | 'AUTHORIZATION'
-
           | 'AUTO'
-
           | 'AUTO_INCREMENT'
-
           | 'AZURE'
-
           | 'BASE64'
-
           | 'BEFORE'
-
           | 'BEGIN'
-
           | 'BERNOULLI'
-
           | 'BINARY'
-
           | 'BIT'
-
           | 'BLOBSTORAGE'
-
           | 'BLOCK'
-
           | 'BOOLEAN'
-
           | 'BREADTH'
-
           | 'BRANCH'
-
           | 'BROWSE'
-
           | 'BY'
-
           | 'BYTES'
-
           | 'CACHE'
-
           | 'BUFFERS'
-
           | 'BYTE'
-
           | 'CALL'
-
           | 'CASCADE'
-
           | 'CASE'
-
           | 'CAST'
-
           | 'CERTIFICATE'
-
           | 'CHARACTER'
-
           | 'CHANGE'
-
           | 'CHANGES'
-
           | 'CHECKPOINT'
-
           | 'CHAR'
-
           | 'CLOSE'
-
           | 'CLOUD'
-
           | 'COALESCE'
-
           | 'COLLATE'
-
           | 'COLUMN'
-
           | 'COLUMNS'
-
           | 'COMMIT'
-
           | 'COMMENT'
-
           | 'COMMENTS'
-
           | 'CONFLICT'
-
           | 'CONSTRAINTS'
-
           | 'CONVERT'
-
           | 'CORRESPONDING'
-
           | 'COSTS'
-
           | 'COUNT'
-
           | 'CREATED'
-
           | 'CYCLE'
-
           | 'DATABASE'
-
           | 'DATA'
-
           | 'DECLARE'
-
           | 'DBA_RECYCLEBIN'
-
           | 'DEFAULTS'
-
           | 'DEPTH'
-
           | 'DEFERRABLE'
-
           | 'DELAYED'
-
           | 'DELETE'
-
           | 'DELIMIT'
-
           | 'DELIMITER'
-
           | 'DESC'
-
           | 'DESCRIBE'
-
           | 'DISABLE'
-
           | 'DISCARD'
-
           | 'DISCONNECT'
-
           | 'DIV'
-
           | 'DDL'
-
           | 'DML'
-
           | 'DO'
-
           | 'DOMAIN'
-
           | 'DRIVER'
-
           | 'DROP'
-
           | 'DUMP'
-
           | 'DUPLICATE'
-
           | 'ELEMENTS'
-
           | 'EMIT'
-
           | 'ENABLE'
-
           | 'ENCODING'
-
           | 'ENCRYPTION'
-
           | 'END'
-
           | 'ENFORCED'
-
           | 'ENGINE'
-
           | 'ERROR'
-
           | 'ESCAPE'
-
           | 'EXA'
-
           | 'EXCHANGE'
-
           | 'EXCLUDE'
-
           | 'EXCLUDING'
-
           | 'EXCLUSIVE'
-
           | 'EXEC'
-
           | 'EXECUTE'
-
           | 'EXPLAIN'
-
           | 'EXPLICIT'
-
           | 'EXTENDED'
-
           | 'EXTRACT'
-
           | 'EXPORT'
-
           | K_ISOLATION
-
           | 'FILTER'
-
           | 'FIRST'
-
           | 'FLUSH'
-
           | 'FOLLOWING'
-
           | 'FORMAT'
-
           | 'FULLTEXT'
-
           | 'FUNCTION'
-
           | 'GRANT'
-
           | 'GROUP_CONCAT'
-
           | 'GUARD'
-
           | 'HASH'
-
           | 'HIGH'
-
           | 'HIGH_PRIORITY'
-
           | 'HISTORY'
-
           | 'HOPPING'
-
           | 'IDENTIFIED'
-
           | 'IDENTITY'
-
           | 'INCLUDE'
-
           | 'INCLUDE_NULL_VALUES'
-
           | 'INCLUDING'
-
           | 'INCREMENT'
-
           | 'INDEX'
-
           | 'INFORMATION'
-
           | 'INSERT'
-
           | 'INTERLEAVE'
-
           | 'INTERPRET'
-
           | 'INVALIDATE'
-
           | 'INVERSE'
-
           | 'INVISIBLE'
-
           | 'ISNULL'
-
           | 'JDBC'
-
           | 'JSON'
-
           | 'JSON_OBJECT'
-
           | 'JSON_OBJECTAGG'
-
           | 'JSON_ARRAY'
-
           | 'JSON_ARRAYAGG'
-
           | 'KEEP'
-
           | 'KEY_BLOCK_SIZE'
-
           | 'KEY'
-
           | 'KEYS'
-
           | 'KILL'
-
           | 'FN'
-
           | 'LAST'
-
           | 'LEADING'
-
           | 'LESS'
-
           | 'LEVEL'
-
           | 'LOCAL'
-
           | 'LOCK'
-
           | 'LOCKED'
-
           | 'LINK'
-
           | 'LOG'
-
           | 'LOOP'
-
           | 'LOW'
-
           | 'LOW_PRIORITY'
-
           | 'LTRIM'
-
           | 'MATCH'
-
           | 'MATCH_ANY'
-
           | 'MATCH_ALL'
-
           | 'MATCH_PHRASE'
-
           | 'MATCH_PHRASE_PREFIX'
-
           | 'MATCH_REGEXP'
-
           | 'MATCHED'
-
           | 'MATERIALIZED'
-
           | 'MAX'
-
           | 'MAXVALUE'
-
           | 'MEMBER'
-
           | 'MERGE'
-
           | 'MIN'
-
           | 'MINVALUE'
-
           | 'MODE'
-
           | 'MODIFY'
-
           | 'MOVEMENT'
-
           | 'NAMES'
-
           | 'NAME'
-
           | 'NEVER'
-
           | 'NEXT'
-
           | K_NEXTVAL
-
           | 'NO'
-
           | 'NOCACHE'
-
           | 'NOKEEP'
-
           | 'NOLOCK'
-
           | 'NOMAXVALUE'
-
           | 'NOMINVALUE'
-
           | 'NONE'
-
           | 'NOORDER'
-
           | 'NOTHING'
-
           | 'NOTNULL'
-
           | 'NOVALIDATE'
-
           | 'NULLS'
-
           | 'NOWAIT'
-
           | 'OF'
-
           | 'OFF'
-
           | 'OPEN'
-
           | 'ORA'
-
           | 'ORDINALITY'
-
           | 'OVER'
-
           | 'OVERFLOW'
-
           | 'OVERLAPS'
-
           | 'OVERRIDING'
-
           | 'OVERWRITE'
-
           | 'PADDING'
-
           | 'PARALLEL'
-
           | 'PARENT'
-
           | 'PARSER'
-
           | 'PARTITION'
-
           | 'PARTITIONING'
-
           | 'PATH'
-
           | 'PERCENT'
-
           | 'PLACING'
-
           | 'PLAN'
-
           | 'PLUS'
-
           | 'PRECEDING'
-
           | 'PRIMARY'
-
           | 'POLICY'
-
           | 'PURGE'
-
           | 'QUERY'
-
           | 'QUICK'
-
           | 'QUIESCE'
-
           | 'RANGE'
-
           | 'RAW'
-
           | 'READ'
-
           | 'REBUILD'
-
           | 'RECYCLEBIN'
-
           | 'RECURSIVE'
-
           | 'REFERENCES'
-
           | 'REFRESH'
-
           | 'REGEXP'
-
           | 'REJECT'
-
           | 'RESPECT'
-
           | 'RLIKE'
-
           | 'REGEXP_LIKE'
-
           | 'REGISTER'
-
           | 'REMOTE'
-
           | 'REMOVE'
-
           | 'RENAME'
-
           | 'REORGANIZE'
-
           | 'REPAIR'
-
           | 'REPEATABLE'
-
           | 'REPLACE'
-
           | 'RESET'
-
           | 'RESTART'
-
           | 'RESUMABLE'
-
           | 'RESUME'
-
           | 'RESTRICT'
-
           | 'RESTRICTED'
-
           | 'RETURN'
-
           | 'ROLLBACK'
-
           | 'ROLLUP'
-
           | 'ROOT'
-
           | 'ROW'
-
           | 'ROWS'
-
           | 'RTRIM'
-
           | 'SAFE_CAST'
-
           | 'SAFE_CONVERT'
-
           | 'SAVEPOINT'
-
           | 'SCHEMA'
-
           | 'SEARCH'
-
           | 'SECURE'
-
           | 'SECURITY'
-
           | 'SEED'
-
           | 'SEQUENCE'
-
           | 'SEPARATOR'
-
           | 'SESSION'
-
           | 'SETS'
-
           | 'SHOW'
-
           | 'SHUTDOWN'
-
           | 'SHARE'
-
           | 'SIBLINGS'
-
           | 'SIMILAR'
-
           | 'SIZE'
-
           | 'SKIP'
-
           | 'SPATIAL'
-
           | 'STORED'
-
           | 'STREAM'
-
           | 'STRICT'
-
           | 'STRING'
-
           | 'STRUCT'
-
           | 'SUMMARIZE'
-
           | 'SUSPEND'
-
           | 'SWITCH'
-
           | 'SYMMETRIC'
-
           | 'SYNONYM'
-
           | 'SYSTEM'
-
           | 'SYSTEM_TIME'
-
           | 'SYSTEM_TIMESTAMP'
-
           | 'SYSTEM_VERSION'
-
           | 'TABLE'
-
           | 'TABLESPACE'
-
           | 'TRIGGER'
-
           | 'THEN'
-
           | 'TEMP'
-
           | K_TEXT_LITERAL
-
           | 'TEMPORARY'
-
           | 'THAN'
-
           | K_TIME_KEY_EXPR
-
           | 'TIMEOUT'
-
           | 'TO'
-
           | 'TRIM'
-
           | 'TRUNCATE'
-
           | 'TRY_CAST'
-
           | 'TRY_CONVERT'
-
           | 'TUMBLING'
-
           | 'TYPE'
-
           | 'UNLIMITED'
-
           | 'UNLOGGED'
-
           | 'UPDATE'
-
           | 'UPSERT'
-
           | 'UNQIESCE'
-
           | 'USER'
-
           | 'SIGNED'
-
           | K_STRING_FUNCTION_NAME
-
           | 'UNSIGNED'
-
           | 'VALIDATE'
-
           | 'VALIDATION'
-
           | 'VERBOSE'
-
           | 'VERSION'
-
           | 'VIEW'
-
           | 'VISIBLE'
-
           | 'VOLATILE'
-
           | 'CONCURRENTLY'
-
           | 'WAIT'
-
           | 'WITH TIES'
-
           | 'WITHIN'
-
           | 'WITHOUT'
-
           | 'WITHOUT_ARRAY_WRAPPER'
-
           | 'WORK'
-
           | 'XML'
-
           | 'XMLAGG'
-
           | 'XMLDATA'
-
           | 'XMLSCHEMA'
-
           | 'XMLTEXT'
-
           | 'XSINIL'
-
           | 'YAML'
-
           | 'YES'
-
           | 'ZONE'
-
- Referenced by: -
- - -====================================================================================================================== -KeywordOrIdentifier -====================================================================================================================== - - -.. raw:: html - - - - - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - NAME - - NEXT - - VALUE - - PUBLIC - - STRING - - DATA - - -
- - -
         ::= S_IDENTIFIER
-
           | S_QUOTED_IDENTIFIER
-
           | 'NAME'
-
           | 'NEXT'
-
           | 'VALUE'
-
           | 'PUBLIC'
-
           | 'STRING'
-
           | 'DATA'
-
- - -====================================================================================================================== -Statement -====================================================================================================================== - - -.. raw:: html - - - - - - IF - - Condition - - SingleStatement - - Block - - ST_SEMICOLON - ELSE - - SingleStatement - - Block - - ST_SEMICOLON - - SingleStatement - - Block - - ST_SEMICOLON - - EOF - - UnsupportedStatement - -
- - -
         ::= 'IF' Condition ( SingleStatement | Block ) ST_SEMICOLON? ( 'ELSE' ( SingleStatement | Block ) ST_SEMICOLON? )?
-
           | ( SingleStatement | Block ) ( ST_SEMICOLON | EOF )
-
           | UnsupportedStatement
-
- Not referenced by any. -
- - -====================================================================================================================== -SingleStatement -====================================================================================================================== - - -.. raw:: html - - - - - - WithList - - SelectWithWithItems - - InsertWithWithItems - - UpdateWithWithItems - - DeleteWithWithItems - - Merge - - Select - - TableStatement - - Upsert - - Alter - - RenameTableStatement - - Create - - Drop - - Analyze - - Truncate - - Execute - - Set - - Reset - - Show - - RefreshMaterializedView - - Use - - SavepointStatement - - RollbackStatement - COMMIT - - Comment - - Describe - - Explain - - Declare - - Grant - - PurgeStatement - - SessionStatement - - LockStatement - - Import - - Export - -
- - - -
           | Select
-
           | TableStatement
-
           | Upsert
-
           | Alter
-
           | RenameTableStatement
-
           | Create
-
           | Drop
-
           | Analyze
-
           | Truncate
-
           | Execute
-
           | Set
-
           | Reset
-
           | Show
-
           | RefreshMaterializedView
-
           | Use
-
           | SavepointStatement
-
           | RollbackStatement
-
           | 'COMMIT'
-
           | Comment
-
           | Describe
-
           | Explain
-
           | Declare
-
           | Grant
-
           | PurgeStatement
-
           | SessionStatement
-
           | LockStatement
-
           | Import
-
           | Export
-
- Referenced by: -
- - -====================================================================================================================== -Block -====================================================================================================================== - - -.. raw:: html - - - - - - BEGIN - - ST_SEMICOLON - - SingleStatement - - Block - - ST_SEMICOLON - END - - ST_SEMICOLON - -
- -
Block    ::= 'BEGIN' ST_SEMICOLON* ( ( SingleStatement | Block ) ST_SEMICOLON )+ 'END' ST_SEMICOLON?
-
- Referenced by: -
- - -====================================================================================================================== -Statements -====================================================================================================================== - - -.. raw:: html - - - - - - ST_SEMICOLON - IF - - Condition - - SingleStatement - - Block - - ST_SEMICOLON - ELSE - - SingleStatement - - Block - - ST_SEMICOLON - - SingleStatement - - Block - - ST_SEMICOLON - - EOF - - ST_SEMICOLON - IF - - Condition - - SingleStatement - - Block - - ST_SEMICOLON - ELSE - - SingleStatement - - Block - - ST_SEMICOLON - - UnsupportedStatement - - EOF - -
- - - -
- Not referenced by any. -
- - -====================================================================================================================== -LockStatement -====================================================================================================================== - - -.. raw:: html - - - - - - LOCK - - TABLE - - Table - IN - - ROW - - SHARE - - EXCLUSIVE - - SHARE - - ROW - - EXCLUSIVE - - UPDATE - - EXCLUSIVE - - MODE - - NOWAIT - - WAIT - - S_LONG - -
- - -
         ::= 'LOCK' 'TABLE' Table 'IN' ( 'ROW' ( 'SHARE' | 'EXCLUSIVE' ) | 'SHARE' ( 'ROW' 'EXCLUSIVE' | 'UPDATE' )? - | 'EXCLUSIVE' ) 'MODE' ( 'NOWAIT' | 'WAIT' S_LONG )?
-
- Referenced by: -
- - -====================================================================================================================== -LikeClause -====================================================================================================================== - - -.. raw:: html - - - - - - LIKE - - Table - ( - - ColumnSelectItemsList - ) - - INCLUDING - - EXCLUDING - - DEFAULTS - - INCLUDING - - EXCLUDING - - IDENTITY - - INCLUDING - - EXCLUDING - - COMMENTS - - -
- - -
         ::= 'LIKE' Table ( '(' ColumnSelectItemsList ')' )? ( ( 'INCLUDING' | 'EXCLUDING' ) 'DEFAULTS' )? ( ( 'INCLUDING' | 'EXCLUDING' - ) 'IDENTITY' )? ( ( 'INCLUDING' | 'EXCLUDING' ) 'COMMENTS' )?
-
- Referenced by: -
- - -====================================================================================================================== -Export -====================================================================================================================== - - -.. raw:: html - - - - - - EXPORT - - Table - - ParenthesedColumnList - - ParenthesedSelect - INTO - - ExportIntoItem - -
- - -
- Referenced by: -
- - -====================================================================================================================== -Import -====================================================================================================================== - - -.. raw:: html - - - - - - IMPORT - - INTO - - Table - - ParenthesedColumnList - - ImportColumns - FROM - - ImportFromItem - -
- -
Import   ::= 'IMPORT' ( 'INTO' ( Table ParenthesedColumnList? | ImportColumns ) )? 'FROM' ImportFromItem
-
- Referenced by: -
- - -====================================================================================================================== -SubImport -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - IMPORT - - INTO - - ImportColumns - FROM - - ImportFromItem - ) - - -
- - -
         ::= '(' 'IMPORT' ( 'INTO' ImportColumns )? 'FROM' ImportFromItem ')'
-
- Referenced by: -
- - -====================================================================================================================== -ImportColumns -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - ColumnDefinition - - LikeClause - , - - ) - - -
- - -
         ::= '(' ( ColumnDefinition | LikeClause ) ( ',' ( ColumnDefinition | LikeClause ) )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -ExportIntoItem -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSDestination - - FileDestination - - ScriptSourceDestination - - ErrorClause - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -ImportFromItem -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSSource - - FileSource - - ScriptSourceDestination - - ErrorClause - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -DBMSDestination -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSType - - ConnectionDefinition - TABLE - - Table - - ParenthesedColumnList - - DBMSTableDestinationOptionList - - ImportExportStatement - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -DBMSTableDestinationOption -====================================================================================================================== - - -.. raw:: html - - - - - - REPLACE - - TRUNCATE - - CREATED - - BY - - S_CHAR_LITERAL - -
- - -
         ::= 'REPLACE'
-
           | 'TRUNCATE'
-
           | 'CREATED' 'BY' S_CHAR_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -DBMSTableDestinationOptionList -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSTableDestinationOption - -
- - -
         ::= DBMSTableDestinationOption+
-
- Referenced by: -
- - -====================================================================================================================== -DBMSSource -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSType - - ConnectionDefinition - TABLE - - Table - - ParenthesedColumnList - - ImportExportStatementsList - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -DBMSType -====================================================================================================================== - - -.. raw:: html - - - - - - EXA - - ORA - - JDBC - - DRIVER - - = - - S_CHAR_LITERAL - -
- -
DBMSType ::= 'EXA'
-
           | 'ORA'
-
           | 'JDBC' ( 'DRIVER' '=' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -FileType -====================================================================================================================== - - -.. raw:: html - - - - - - CSV - - FBV - - -
- -
FileType ::= 'CSV'
-
           | 'FBV'
-
- Referenced by: -
- - -====================================================================================================================== -ImportExportStatement -====================================================================================================================== - - -.. raw:: html - - - - - - STATEMENT - - S_CHAR_LITERAL - -
- - -
         ::= 'STATEMENT' S_CHAR_LITERAL
-
- - -====================================================================================================================== -ImportExportStatementsList -====================================================================================================================== - - -.. raw:: html - - - - - - ImportExportStatement - -
- - -
         ::= ImportExportStatement+
-
- Referenced by: -
- - -====================================================================================================================== -File -====================================================================================================================== - - -.. raw:: html - - - - - - FILE - - S_CHAR_LITERAL - -
- -
File     ::= 'FILE' S_CHAR_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -FileList -====================================================================================================================== - - -.. raw:: html - - - - - - File - -
- - -
- - -====================================================================================================================== -ConnectionFileDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - ConnectionOrCloudConnectionDefinition - - FileList - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -ConnectionFileDefinitionList -====================================================================================================================== - - -.. raw:: html - - - - - - ConnectionFileDefinition - -
- - -
         ::= ConnectionFileDefinition+
-
- Referenced by: -
- - -====================================================================================================================== -CSVDestinationColumn -====================================================================================================================== - - -.. raw:: html - - - - - - S_LONG - .. - - S_LONG - FORMAT - - = - - S_CHAR_LITERAL - DELIMIT - - = - - ALWAYS - - NEVER - - AUTO - - -
- - -
         ::= S_LONG ( '..' S_LONG | ( 'FORMAT' '=' S_CHAR_LITERAL )? ( 'DELIMIT' '=' ( 'ALWAYS' | 'NEVER' | 'AUTO' ) )? )
-
- Referenced by: -
- - -====================================================================================================================== -CSVDestinationColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - CSVDestinationColumn - , - - -
- - -
         ::= CSVDestinationColumn ( ',' CSVDestinationColumn )*
-
- Referenced by: -
- - -====================================================================================================================== -CSVSourceColumn -====================================================================================================================== - - -.. raw:: html - - - - - - S_LONG - .. - - S_LONG - FORMAT - - = - - S_CHAR_LITERAL - -
- - -
         ::= S_LONG ( '..' S_LONG | 'FORMAT' '=' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -CSVSourceColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - CSVSourceColumn - , - - -
- - -
         ::= CSVSourceColumn ( ',' CSVSourceColumn )*
-
- Referenced by: -
- - -====================================================================================================================== -FBVDestinationColumn -====================================================================================================================== - - -.. raw:: html - - - - - - SIZE - - = - - S_LONG - FORMAT - - PADDING - - = - - S_CHAR_LITERAL - ALIGN - - = - - LEFT - - RIGHT - - -
- - -
         ::= 'SIZE' '=' S_LONG
-
           | ( 'FORMAT' | 'PADDING' ) '=' S_CHAR_LITERAL
-
           | 'ALIGN' '=' ( 'LEFT' | 'RIGHT' )
-
- Referenced by: -
- - -====================================================================================================================== -FBVDestinationColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - FBVDestinationColumn - , - - -
- - -
         ::= FBVDestinationColumn ( ','? FBVDestinationColumn )*
-
- Referenced by: -
- - -====================================================================================================================== -FBVSourceColumn -====================================================================================================================== - - -.. raw:: html - - - - - - SIZE - - START - - = - - S_LONG - FORMAT - - PADDING - - = - - S_CHAR_LITERAL - ALIGN - - = - - LEFT - - RIGHT - - -
- - -
         ::= ( 'SIZE' | 'START' ) '=' S_LONG
-
           | ( 'FORMAT' | 'PADDING' ) '=' S_CHAR_LITERAL
-
           | 'ALIGN' '=' ( 'LEFT' | 'RIGHT' )
-
- Referenced by: -
- - -====================================================================================================================== -FBVSourceColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - FBVSourceColumn - , - - -
- - -
         ::= FBVSourceColumn ( ','? FBVSourceColumn )*
-
- Referenced by: -
- - -====================================================================================================================== -FileDestinationOption -====================================================================================================================== - - -.. raw:: html - - - - - - REPLACE - - TRUNCATE - - WITH - - COLUMN - - NAMES - - ENCODING - - NULL - - BOOLEAN - - ROW - - SEPARATOR - - COLUMN - - SEPARATOR - - DELIMITER - - = - - S_CHAR_LITERAL - DELIMIT - - = - - ALWAYS - - NEVER - - AUTO - - -
- - -
         ::= 'REPLACE'
-
           | 'TRUNCATE'
-
           | 'WITH' 'COLUMN' 'NAMES'
-
           | ( 'ENCODING' | 'NULL' | 'BOOLEAN' | 'ROW' 'SEPARATOR' | 'COLUMN' ( 'SEPARATOR' - | 'DELIMITER' ) ) '=' S_CHAR_LITERAL
-
           | 'DELIMIT' '=' ( 'ALWAYS' | 'NEVER' | 'AUTO' )
-
- Referenced by: -
- - -====================================================================================================================== -FileDestinationOptionList -====================================================================================================================== - - -.. raw:: html - - - - - - FileDestinationOption - -
- - -
         ::= FileDestinationOption+
-
- Referenced by: -
- - -====================================================================================================================== -FileSourceOption -====================================================================================================================== - - -.. raw:: html - - - - - - TRIM - - LTRIM - - RTRIM - - ENCODING - - NULL - - COLUMN - - SEPARATOR - - DELIMITER - - = - - S_CHAR_LITERAL - SKIP - - = - - S_LONG - ROW - - SEPARATOR - - = - - S_CHAR_LITERAL - SIZE - - = - - S_LONG - -
- - -
         ::= 'TRIM'
-
           | 'LTRIM'
-
           | 'RTRIM'
-
           | ( 'ENCODING' | 'NULL' | 'COLUMN' ( 'SEPARATOR' | 'DELIMITER' ) ) '=' - S_CHAR_LITERAL
-
           | 'SKIP' '=' S_LONG
-
           | 'ROW' ( 'SEPARATOR' '=' S_CHAR_LITERAL | 'SIZE' '=' S_LONG )
-
- Referenced by: -
- - -====================================================================================================================== -FileSourceOptionList -====================================================================================================================== - - -.. raw:: html - - - - - - FileSourceOption - -
- - -
         ::= FileSourceOption+
-
- Referenced by: -
- - -====================================================================================================================== -FileDestination -====================================================================================================================== - - -.. raw:: html - - - - - - FileType - - ConnectionFileDefinitionList - LOCAL - - SECURE - - FileType - - FileList - ( - - CSVDestinationColumnList - - FBVDestinationColumnList - ) - - FileDestinationOptionList - - CertificateVerification - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -FileSource -====================================================================================================================== - - -.. raw:: html - - - - - - FileType - - ConnectionFileDefinitionList - LOCAL - - SECURE - - FileType - - FileList - ( - - CSVSourceColumnList - - FBVSourceColumnList - ) - - FileSourceOptionList - - CertificateVerification - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -CertificateVerification -====================================================================================================================== - - -.. raw:: html - - - - - - IGNORE - - VERIFY - - CERTIFICATE - - PUBLIC - - KEY - - S_CHAR_LITERAL - PUBLIC - - KEY - - S_CHAR_LITERAL - -
- - -
         ::= ( 'IGNORE' | 'VERIFY' ) 'CERTIFICATE' ( 'PUBLIC' 'KEY' S_CHAR_LITERAL )?
-
           | 'PUBLIC' 'KEY' S_CHAR_LITERAL
-
- - -====================================================================================================================== -ScriptSourceDestination -====================================================================================================================== - - -.. raw:: html - - - - - - SCRIPT - - Table - - ConnectionDefinition - WITH - - RelObjectName - = - - S_CHAR_LITERAL - -
- - -
         ::= 'SCRIPT' Table ConnectionDefinition? ( 'WITH' ( RelObjectName '=' S_CHAR_LITERAL )+ )?
-
- Referenced by: -
- - -====================================================================================================================== -UserIdentification -====================================================================================================================== - - -.. raw:: html - - - - - - USER - - S_CHAR_LITERAL - IDENTIFIED - - BY - - S_CHAR_LITERAL - -
- - -
         ::= 'USER' S_CHAR_LITERAL 'IDENTIFIED' 'BY' S_CHAR_LITERAL
-
- - -====================================================================================================================== -ConnectionDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - AT - - RelObjectName - - S_CHAR_LITERAL - - UserIdentification - - CertificateVerification - -
- - - -
- - -====================================================================================================================== -CloudConnectionDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - AT - - CLOUD - - NONE - - AZURE - - BLOBSTORAGE - - RelObjectName - - S_CHAR_LITERAL - - UserIdentification - -
- - -
         ::= 'AT' 'CLOUD' ( 'NONE' | 'AZURE' 'BLOBSTORAGE' ) ( RelObjectName | S_CHAR_LITERAL ) UserIdentification?
-
- - -====================================================================================================================== -ConnectionOrCloudConnectionDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - CloudConnectionDefinition - - ConnectionDefinition - -
- - -
         ::= CloudConnectionDefinition
-
           | ConnectionDefinition
-
- - -====================================================================================================================== -ErrorClause -====================================================================================================================== - - -.. raw:: html - - - - - - ERRORS - - INTO - - ErrorDestination - ( - - Expression - ) - - REPLACE - - TRUNCATE - - RejectClause - - RejectClause - -
- - -
         ::= 'ERRORS' 'INTO' ErrorDestination ( '(' Expression ')' )? ( 'REPLACE' | 'TRUNCATE' )? RejectClause?
-
           | RejectClause
-
- Referenced by: -
- - -====================================================================================================================== -RejectClause -====================================================================================================================== - - -.. raw:: html - - - - - - REJECT - - LIMIT - - S_LONG - UNLIMITED - - ERRORS - - -
- - -
         ::= 'REJECT' 'LIMIT' ( S_LONG | 'UNLIMITED' ) 'ERRORS'?
-
- Referenced by: -
- - -====================================================================================================================== -ErrorDestination -====================================================================================================================== - - -.. raw:: html - - - - - - CSVFileDestination - - Table - -
- - -
         ::= CSVFileDestination
-
           | Table
-
- Referenced by: -
- - -====================================================================================================================== -CSVFileDestination -====================================================================================================================== - - -.. raw:: html - - - - - - CSV - - ConnectionOrCloudConnectionDefinition - LOCAL - - SECURE - - CSV - - File - -
- - -
         ::= ( 'CSV' ConnectionOrCloudConnectionDefinition | 'LOCAL' 'SECURE'? 'CSV' ) File
-
- Referenced by: -
- - -====================================================================================================================== -Declare -====================================================================================================================== - - -.. raw:: html - - - - - - DECLARE - - UserVariable - TABLE - - ( - - ColumnDefinition - , - - ) - - AS - - RelObjectName - - ColDataType - = - - Expression - - UserVariable - , - - -
- -
Declare  ::= 'DECLARE' UserVariable ( 'TABLE' '(' ColumnDefinition ( ',' ColumnDefinition )* ')' | 'AS' RelObjectName | ColDataType ( '=' Expression )? ( ',' UserVariable ColDataType ( '=' Expression )? )* )
-
- Referenced by: -
- - -====================================================================================================================== -SessionStatement -====================================================================================================================== - - -.. raw:: html - - - - - - SESSION - - BRANCH - - START - - APPLY - - DROP - - SHOW - - DESCRIBE - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - S_CHAR_LITERAL - - S_LONG - . - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - S_CHAR_LITERAL - - S_LONG - WITH - - S_IDENTIFIER - KEEP - - = - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - S_CHAR_LITERAL - - S_LONG - TRUE - - FALSE - - ON - - OFF - - YES - - NO - - , - - -
- - -
         ::= ( 'SESSION' | 'BRANCH' ) ( 'START' | 'APPLY' | 'DROP' | 'SHOW' | 'DESCRIBE' - ) ( ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | S_CHAR_LITERAL | S_LONG ) ( '.' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | S_CHAR_LITERAL | S_LONG ) )? )? ( 'WITH' ( S_IDENTIFIER | 'KEEP' ) '=' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | S_CHAR_LITERAL | S_LONG | 'TRUE' | 'FALSE' | 'ON' | 'OFF' | 'YES' | 'NO' ) ( ',' ( S_IDENTIFIER | 'KEEP' ) '=' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | S_CHAR_LITERAL | S_LONG | 'TRUE' | 'FALSE' | 'ON' | 'OFF' | 'YES' | 'NO' ) )* )?
-
- Referenced by: -
- - -====================================================================================================================== -Set -====================================================================================================================== - - -.. raw:: html - - - - - - SET - - LOCAL - - SESSION - - K_DATETIMELITERAL - ZONE - - UserVariable - - IdentifierChain - = - - Expression - ZONE - - K_DATETIMELITERAL - = - - RelObjectName - , - - -
- -
Set      ::= 'SET' ( 'LOCAL' | 'SESSION' )? ( K_DATETIMELITERAL 'ZONE' | ( UserVariable | IdentifierChain ) '='? ) Expression ( ',' ( K_DATETIMELITERAL 'ZONE' | RelObjectName '='? )? Expression )*
-
- Referenced by: -
- - -====================================================================================================================== -Reset -====================================================================================================================== - - -.. raw:: html - - - - - - RESET - - K_DATETIMELITERAL - ZONE - - RelObjectName - ALL - - -
- -
Reset    ::= 'RESET' ( K_DATETIMELITERAL 'ZONE' | RelObjectName | 'ALL' )
-
- Referenced by: -
- - -====================================================================================================================== -RenameTableStatement -====================================================================================================================== - - -.. raw:: html - - - - - - RENAME - - TABLE - - IF - - EXISTS - - Table - WAIT - - S_LONG - NOWAIT - - TO - - Table - - Table - , - - -
- - -
         ::= 'RENAME' 'TABLE'? ( 'IF' 'EXISTS' )? Table ( 'WAIT' S_LONG | 'NOWAIT' )? 'TO' Table ( ',' Table 'TO' Table )*
-
- Referenced by: -
- - -====================================================================================================================== -PurgeStatement -====================================================================================================================== - - -.. raw:: html - - - - - - PURGE - - TABLE - - Table - INDEX - - Index - RECYCLEBIN - - DBA_RECYCLEBIN - - TABLESPACE - - S_IDENTIFIER - USER - - S_IDENTIFIER - -
- - -
         ::= 'PURGE' ( 'TABLE' Table | 'INDEX' Index | 'RECYCLEBIN' | 'DBA_RECYCLEBIN' | 'TABLESPACE' S_IDENTIFIER ( 'USER' S_IDENTIFIER )? )
-
- Referenced by: -
- - -====================================================================================================================== -Describe -====================================================================================================================== - - -.. raw:: html - - - - - - DESCRIBE - - DESC - - Table - -
- -
Describe ::= ( 'DESCRIBE' | 'DESC' ) Table
-
- Referenced by: -
- - -====================================================================================================================== -Explain -====================================================================================================================== - - -.. raw:: html - - - - - - EXPLAIN - - SUMMARIZE - - ExplainStatementOptions - - WithList - - SelectWithWithItems - - InsertWithWithItems - - UpdateWithWithItems - - DeleteWithWithItems - - Merge - - Table - -
- - -
- Referenced by: -
- - -====================================================================================================================== -ExplainOptionBoolean -====================================================================================================================== - - -.. raw:: html - - - - - - TRUE - - FALSE - - ON - - OFF - - -
- - -
         ::= ( 'TRUE' | 'FALSE' | 'ON' | 'OFF' )?
-
- Referenced by: -
- - -====================================================================================================================== -ExplainFormatOption -====================================================================================================================== - - -.. raw:: html - - - - - - XML - - JSON - - YAML - - -
- - -
         ::= ( 'XML' | 'JSON' | 'YAML' )?
-
- Referenced by: -
- - -====================================================================================================================== -ExplainStatementOptions -====================================================================================================================== - - -.. raw:: html - - - - - - ANALYZE - - BUFFERS - - COSTS - - VERBOSE - - ExplainOptionBoolean - FORMAT - - PLAN - - FOR - - ExplainFormatOption - -
- - -
         ::= ( ( 'ANALYZE' | 'BUFFERS' | 'COSTS' | 'VERBOSE' ) ExplainOptionBoolean | ( 'FORMAT' | 'PLAN' 'FOR'? ) ExplainFormatOption )*
-
- Referenced by: -
- - -====================================================================================================================== -Use -====================================================================================================================== - - -.. raw:: html - - - - - - USE - - SCHEMA - - RelObjectName - -
- -
Use      ::= 'USE' 'SCHEMA'? RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -Show -====================================================================================================================== - - -.. raw:: html - - - - - - SHOW - - ShowColumns - - ShowIndex - - ShowTables - - captureRest - -
- -
Show     ::= 'SHOW' ( ShowColumns | ShowIndex | ShowTables | captureRest )
-
- Referenced by: -
- - -====================================================================================================================== -ShowColumns -====================================================================================================================== - - -.. raw:: html - - - - - - COLUMNS - - FROM - - RelObjectName - -
- - -
         ::= 'COLUMNS' 'FROM' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -ShowIndex -====================================================================================================================== - - -.. raw:: html - - - - - - INDEX - - FROM - - RelObjectName - -
- - -
         ::= 'INDEX' 'FROM' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -RefreshMaterializedView -====================================================================================================================== - - -.. raw:: html - - - - - - REFRESH - - MATERIALIZED - - VIEW - - CONCURRENTLY - - Table - WITH - - NO - - DATA - - captureRest - -
- - -
         ::= 'REFRESH' 'MATERIALIZED' 'VIEW' 'CONCURRENTLY'? Table ( 'WITH' 'NO'? 'DATA' )? captureRest
-
- Referenced by: -
- - -====================================================================================================================== -ShowTables -====================================================================================================================== - - -.. raw:: html - - - - - - EXTENDED - - FULL - - TABLES - - FROM - - IN - - RelObjectName - LIKE - - SimpleExpression - WHERE - - Expression - -
- - -
         ::= 'EXTENDED'? 'FULL'? 'TABLES' ( ( 'FROM' | 'IN' ) RelObjectName )? ( 'LIKE' SimpleExpression | 'WHERE' Expression )?
-
- Referenced by: -
- - -====================================================================================================================== -Values -====================================================================================================================== - - -.. raw:: html - - - - - - VALUES - - VALUE - - ExpressionList - -
- -
Values   ::= ( 'VALUES' | 'VALUE' ) ExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -ReturningClause -====================================================================================================================== - - -.. raw:: html - - - - - - RETURNING - - RETURN - - ReturningOutputAliasList - - SelectItemsList - INTO - - Table - - UserVariable - , - - -
- - -
         ::= ( 'RETURNING' | 'RETURN' ) ReturningOutputAliasList? SelectItemsList ( 'INTO' ( Table | UserVariable ) ( ',' ( Table | UserVariable ) )* )?
-
- Referenced by: -
- - -====================================================================================================================== -ReturningReferenceKind -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - -
- - -
         ::= RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -ReturningOutputAliasDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - ReturningReferenceKind - AS - - RelObjectName - -
- - -
         ::= ReturningReferenceKind 'AS' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -ReturningOutputAliasList -====================================================================================================================== - - -.. raw:: html - - - - - - WITH - - ( - - ReturningOutputAliasDefinition - , - - ) - - -
- - -
         ::= 'WITH' '(' ReturningOutputAliasDefinition ( ',' ReturningOutputAliasDefinition )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -UpdateWithWithItems -====================================================================================================================== - - -.. raw:: html - - - - - - Update - -
- - -
         ::= Update
-
- Referenced by: -
- - -====================================================================================================================== -Update -====================================================================================================================== - - -.. raw:: html - - - - - - UPDATE - - LOW_PRIORITY - - IGNORE - - TableWithAliasAndMysqlIndexHint - - JoinsList - SET - - UpdateSets - - OutputClause - FROM - - FromItem - - JoinsList - - WhereClause - - PreferringClause - - OrderByElements - - PlainLimit - - ReturningClause - -
- - -
- - -====================================================================================================================== -UpdateSets -====================================================================================================================== - - -.. raw:: html - - - - - - Column - = - - Expression - - ParenthesedExpressionList - = - - ParenthesedSelect - - ParenthesedExpressionList - , - - Column - = - - Expression - - ParenthesedExpressionList - = - - ParenthesedSelect - - ParenthesedExpressionList - -
- - - -
- - -====================================================================================================================== -Partitions -====================================================================================================================== - - -.. raw:: html - - - - - - Column - = - - Expression - , - - -
- - -
         ::= Column ( '=' Expression )? ( ',' Column ( '=' Expression )? )*
-
- Referenced by: -
- - -====================================================================================================================== -InsertWithWithItems -====================================================================================================================== - - -.. raw:: html - - - - - - Insert - -
- - -
         ::= Insert
-
- Referenced by: -
- - -====================================================================================================================== -Insert -====================================================================================================================== - - -.. raw:: html - - - - - - INSERT - - LOW_PRIORITY - - DELAYED - - HIGH_PRIORITY - - IGNORE - - ALL - - FIRST - - OracleMultiInsertClause - - OracleMultiInsertWhenBranch - - OracleMultiInsertElseBranch - - Select - OVERWRITE - - TABLE - - INTO - - TABLE - - Table - PARTITION - - ( - - Partitions - ) - - AS - - RelObjectName - ( - - ColumnList - ) - - OVERRIDING - - SYSTEM - - VALUE - - OutputClause - DEFAULT - - VALUES - - SET - - UpdateSets - - Select - - Alias - ON - - DUPLICATE - - KEY - - UPDATE - - InsertDuplicateAction - ON - - CONFLICT - - InsertConflictTarget - - InsertConflictAction - - ReturningClause - -
- -
Insert   ::= 'INSERT' ( 'LOW_PRIORITY' | 'DELAYED' | 'HIGH_PRIORITY' )? 'IGNORE'? ( ( 'ALL' - | 'FIRST' ) ( OracleMultiInsertClause+ | OracleMultiInsertWhenBranch+ OracleMultiInsertElseBranch? ) Select | ( 'OVERWRITE' 'TABLE' | 'INTO' 'TABLE'? )? Table ( 'PARTITION' '(' Partitions ')' )? ( 'AS'? RelObjectName )? ( '(' ColumnList ')' )? ( 'OVERRIDING' 'SYSTEM' 'VALUE' )? OutputClause? ( 'DEFAULT' 'VALUES' | 'SET' UpdateSets | Select ) Alias? ( 'ON' 'DUPLICATE' 'KEY' 'UPDATE' InsertDuplicateAction )? ( 'ON' 'CONFLICT' InsertConflictTarget? InsertConflictAction )? ReturningClause? )
-
- - -====================================================================================================================== -OracleMultiInsertClause -====================================================================================================================== - - -.. raw:: html - - - - - - INTO - - Table - ( - - ColumnList - ) - - Select - -
- - -
         ::= 'INTO' Table ( '(' ColumnList ')' )? Select
-
- - -====================================================================================================================== -OracleMultiInsertWhenBranch -====================================================================================================================== - - -.. raw:: html - - - - - - WHEN - - Expression - THEN - - OracleMultiInsertClauseList - -
- - -
         ::= 'WHEN' Expression 'THEN' OracleMultiInsertClauseList
-
- Referenced by: -
- - -====================================================================================================================== -OracleMultiInsertElseBranch -====================================================================================================================== - - -.. raw:: html - - - - - - ELSE - - OracleMultiInsertClauseList - -
- - -
         ::= 'ELSE' OracleMultiInsertClauseList
-
- Referenced by: -
- - -====================================================================================================================== -OracleMultiInsertClauseList -====================================================================================================================== - - -.. raw:: html - - - - - - OracleMultiInsertClause - -
- - -
         ::= OracleMultiInsertClause+
-
- - -====================================================================================================================== -InsertConflictTarget -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - RelObjectNameExt - , - - ) - - WhereClause - ON - - CONSTRAINT - - RelObjectNameExt - -
- - -
         ::= '(' RelObjectNameExt ( ',' RelObjectNameExt )* ')' WhereClause?
-
           | 'ON' 'CONSTRAINT' RelObjectNameExt
-
- Referenced by: -
- - -====================================================================================================================== -InsertConflictAction -====================================================================================================================== - - -.. raw:: html - - - - - - DO - - NOTHING - - UPDATE - - SET - - UpdateSets - - WhereClause - -
- - -
         ::= 'DO' ( 'NOTHING' | 'UPDATE' 'SET' UpdateSets WhereClause? )
-
- Referenced by: -
- - -====================================================================================================================== -InsertDuplicateAction -====================================================================================================================== - - -.. raw:: html - - - - - - NOTHING - - UpdateSets - - WhereClause - -
- - -
         ::= 'NOTHING'
-
           | UpdateSets WhereClause?
-
- Referenced by: -
- - -====================================================================================================================== -OutputClause -====================================================================================================================== - - -.. raw:: html - - - - - - OUTPUT - - SelectItemsList - INTO - - UserVariable - - Table - - ColumnsNamesList - -
- - -
         ::= 'OUTPUT' SelectItemsList ( 'INTO' ( UserVariable | Table ) ColumnsNamesList? )?
-
- Referenced by: -
- - -====================================================================================================================== -Upsert -====================================================================================================================== - - -.. raw:: html - - - - - - UPSERT - - INSERT - - OR - - REPLACE - - INTO - - Table - - ParenthesedColumnList - SET - - UpdateSets - - Select - ON - - DUPLICATE - - KEY - - UPDATE - - InsertDuplicateAction - -
- -
Upsert   ::= ( 'UPSERT' | ( 'INSERT' 'OR' )? 'REPLACE' ) 'INTO'? Table ParenthesedColumnList? ( 'SET' UpdateSets | Select ) ( 'ON' 'DUPLICATE' 'KEY' 'UPDATE' InsertDuplicateAction )?
-
- Referenced by: -
- - -====================================================================================================================== -DeleteWithWithItems -====================================================================================================================== - - -.. raw:: html - - - - - - Delete - -
- - -
         ::= Delete
-
- Referenced by: -
- - -====================================================================================================================== -Delete -====================================================================================================================== - - -.. raw:: html - - - - - - DELETE - - LOW_PRIORITY - - QUICK - - IGNORE - - TableWithAlias - , - - OutputClause - FROM - - TableWithAlias - - JoinsList - USING - - FromItem - , - - WhereClause - - PreferringClause - - OrderByElements - - PlainLimit - - ReturningClause - -
- -
Delete   ::= 'DELETE' 'LOW_PRIORITY'? 'QUICK'? 'IGNORE'? ( ( TableWithAlias ( ',' TableWithAlias )* OutputClause? )? 'FROM' )? ( TableWithAlias JoinsList? )? ( 'USING' FromItem ( ',' FromItem )* )? WhereClause? PreferringClause? OrderByElements? PlainLimit? ReturningClause?
-
- - -====================================================================================================================== -Merge -====================================================================================================================== - - -.. raw:: html - - - - - - MERGE - - INTO - - TableWithAlias - USING - - FromItem - ON - - Expression - - MergeOperations - - OutputClause - -
- -
Merge    ::= 'MERGE' 'INTO' TableWithAlias 'USING' FromItem 'ON' Expression MergeOperations OutputClause?
-
- Referenced by: -
- - -====================================================================================================================== -MergeOperations -====================================================================================================================== - - -.. raw:: html - - - - - - MergeWhenMatched - - MergeWhenNotMatched - -
- - -
         ::= ( MergeWhenMatched | MergeWhenNotMatched )*
-
- Referenced by: -
- - -====================================================================================================================== -MergeWhenMatched -====================================================================================================================== - - -.. raw:: html - - - - - - WHEN - - MATCHED - - AND - - Expression - THEN - - DELETE - - MergeUpdateClause - -
- - -
         ::= 'WHEN' 'MATCHED' ( 'AND' Expression )? 'THEN' ( 'DELETE' | MergeUpdateClause )
-
- Referenced by: -
- - -====================================================================================================================== -MergeUpdateClause -====================================================================================================================== - - -.. raw:: html - - - - - - UPDATE - - SET - - UpdateSets - WHERE - - Expression - DELETE - - WHERE - - Expression - -
- - -
         ::= 'UPDATE' 'SET' UpdateSets ( 'WHERE' Expression )? ( 'DELETE' 'WHERE' Expression )?
-
- Referenced by: -
- - -====================================================================================================================== -MergeWhenNotMatched -====================================================================================================================== - - -.. raw:: html - - - - - - WHEN - - NOT - - MATCHED - - AND - - Expression - THEN - - INSERT - - ( - - ColumnList - ) - - VALUES - - ( - - SimpleExpressionList - ) - - WHERE - - Expression - -
- - -
         ::= 'WHEN' 'NOT' 'MATCHED' ( 'AND' Expression )? 'THEN' 'INSERT' ( '(' ColumnList ')' )? 'VALUES' '(' SimpleExpressionList ')' ( 'WHERE' Expression )?
-
- Referenced by: -
- - -====================================================================================================================== -RelObjectNames -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ... - - .. - - . - - : - - RelObjectNameExt - -
- - -
         ::= RelObjectName ( ( '...' | '..' | '.' | ':' ) RelObjectNameExt )*
-
- - -====================================================================================================================== -ColumnIdentifier -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ... - - .. - - . - - RelObjectNameExt - -
- - -
         ::= RelObjectName ( ( '...' | '..' | '.' ) RelObjectNameExt )*
-
- Referenced by: -
- - -====================================================================================================================== -Column -====================================================================================================================== - - -.. raw:: html - - - - - - ColumnIdentifier - COMMENT - - S_CHAR_LITERAL - . - - K_NEXTVAL - - ArrayConstructor - -
- - -
- - -====================================================================================================================== -RelObjectName -====================================================================================================================== - - -.. raw:: html - - - - - - DATA_TYPE - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - K_DATETIMELITERAL - - K_DATE_LITERAL - - NonReservedWord - ALL - - ANY - - CASEWHEN - - CONNECT - - CREATE - - DEFAULT - - GLOBAL - - GROUP - - GROUPING - - IF - - IIF - - IGNORE - - IN - - INTERVAL - - LEFT - - LIMIT - - K_NEXTVAL - OFFSET - - ON - - OPTIMIZE - - ORDER - - PROCEDURE - - PUBLIC - - QUALIFY - - RIGHT - - SET - - SOME - - START - - TABLES - - TOP - - VALUE - - VALUES - - -
- - -
         ::= DATA_TYPE
-
           | S_IDENTIFIER
-
           | S_QUOTED_IDENTIFIER
-
           | K_DATETIMELITERAL
-
           | K_DATE_LITERAL
-
           | NonReservedWord
-
           | 'ALL'
-
           | 'ANY'
-
           | 'CASEWHEN'
-
           | 'CONNECT'
-
           | 'CREATE'
-
           | 'DEFAULT'
-
           | 'GLOBAL'
-
           | 'GROUP'
-
           | 'GROUPING'
-
           | 'IF'
-
           | 'IIF'
-
           | 'IGNORE'
-
           | 'IN'
-
           | 'INTERVAL'
-
           | 'LEFT'
-
           | 'LIMIT'
-
           | K_NEXTVAL
-
           | 'OFFSET'
-
           | 'ON'
-
           | 'OPTIMIZE'
-
           | 'ORDER'
-
           | 'PROCEDURE'
-
           | 'PUBLIC'
-
           | 'QUALIFY'
-
           | 'RIGHT'
-
           | 'SET'
-
           | 'SOME'
-
           | 'START'
-
           | 'TABLES'
-
           | 'TOP'
-
           | 'VALUE'
-
           | 'VALUES'
-
- - -====================================================================================================================== -RelObjectNameExt -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - FROM - - K_SELECT - CURRENT - - -
- - -
         ::= RelObjectName
-
           | 'FROM'
-
           | K_SELECT
-
           | 'CURRENT'
-
- - -====================================================================================================================== -Table -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNames - - TimeTravelBeforeAlias - - S_CHAR_LITERAL - -
- - -
           | S_CHAR_LITERAL
-
- - -====================================================================================================================== -TableWithAlias -====================================================================================================================== - - -.. raw:: html - - - - - - Table - - Alias - -
- - -
         ::= Table Alias?
-
- Referenced by: -
- - -====================================================================================================================== -TableWithAliasAndMysqlIndexHint -====================================================================================================================== - - -.. raw:: html - - - - - - Table - - Alias - - MySQLIndexHint - -
- - -
         ::= Table Alias? MySQLIndexHint?
-
- Referenced by: -
- - -====================================================================================================================== -Number -====================================================================================================================== - - -.. raw:: html - - - - - - S_DOUBLE - - S_LONG - -
- -
Number   ::= S_DOUBLE
-
           | S_LONG
-
- Referenced by: -
- - -====================================================================================================================== -SampleClause -====================================================================================================================== - - -.. raw:: html - - - - - - SAMPLE - - BLOCK - - TABLESAMPLE - - USING - - SAMPLE - - SYSTEM - - BERNOULLI - - ( - - Number - % - - PERCENT - - ROWS - - ) - - REPEATABLE - - ( - - Number - ) - - SEED - - ( - - Number - ) - - Number - OFFSET - - Number - -
- - -
         ::= ( 'SAMPLE' 'BLOCK'? | ( 'TABLESAMPLE' | 'USING' 'SAMPLE' ) ( 'SYSTEM' - | 'BERNOULLI' ) ) ( '(' Number ( '%' | 'PERCENT' | 'ROWS' )? ')' ( 'REPEATABLE' '(' Number ')' )? ( 'SEED' '(' Number ')' )? | Number ( 'OFFSET' Number )? )
-
- Referenced by: -
- - -====================================================================================================================== -SelectWithWithItems -====================================================================================================================== - - -.. raw:: html - - - - - - Select - -
- - -
         ::= Select
-
- Referenced by: -
- - -====================================================================================================================== -Select -====================================================================================================================== - - -.. raw:: html - - - - - - WithList - - FromQuery - - PlainSelect - - Values - - ParenthesedSelect - - Alias - - FromQueryFromSelect - - SetOperationList - - OrderByElements - - LimitWithOffset - - Offset - - Fetch - - WithIsolation - -
- - -
- - -====================================================================================================================== -FromQuery -====================================================================================================================== - - -.. raw:: html - - - - - - FROM - - FromItem - - LateralViews - - JoinsList - |> - - PipeOperator - -
- - -
         ::= 'FROM' FromItem LateralViews? JoinsList? ( '|>' PipeOperator )*
-
- Referenced by: -
- - -====================================================================================================================== -FromQueryFromSelect -====================================================================================================================== - - -.. raw:: html - - - - - - |> - - PipeOperator - -
- - -
         ::= ( '|>' PipeOperator )+
-
- Referenced by: -
- - -====================================================================================================================== -PipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - SelectPipeOperator - - SetPipeOperator - - DropPipeOperator - - AsPipeOperator - - WherePipeOperator - - LimitPipeOperator - - AggregatePipeOperator - - OrderByPipeOperator - - SetOperationPipeOperator - - JoinPipeOperator - - CallPipeOperator - - TableSamplePipeOperator - - PivotPipeOperator - - UnPivotPipeOperator - -
- - -
         ::= SelectPipeOperator
-
           | SetPipeOperator
-
           | DropPipeOperator
-
           | AsPipeOperator
-
           | WherePipeOperator
-
           | LimitPipeOperator
-
           | AggregatePipeOperator
-
           | OrderByPipeOperator
-
           | SetOperationPipeOperator
-
           | JoinPipeOperator
-
           | CallPipeOperator
-
           | TableSamplePipeOperator
-
           | PivotPipeOperator
-
           | UnPivotPipeOperator
-
- Referenced by: -
- - -====================================================================================================================== -SelectPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - K_SELECT - DISTINCT - - ALL - - EXTEND - - WINDOW - - RENAME - - SelectItem - , - - -
- - -
         ::= ( K_SELECT ( 'DISTINCT' | 'ALL' )? | 'EXTEND' | 'WINDOW' | 'RENAME' ) SelectItem ( ',' SelectItem )*
-
- Referenced by: -
- - -====================================================================================================================== -WherePipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - WHERE - - Expression - -
- - -
         ::= 'WHERE' Expression
-
- Referenced by: -
- - -====================================================================================================================== -OrderSuffix -====================================================================================================================== - - -.. raw:: html - - - - - - ASC - - DESC - - NULLS - - FIRST - - LAST - - -
- - -
         ::= ( 'ASC' | 'DESC' ) ( 'NULLS' ( 'FIRST' | 'LAST' ) )?
-
- Referenced by: -
- - -====================================================================================================================== -AggregatePipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - AGGREGATE - - SelectItem - - OrderSuffix - , - - GROUP - - AND - - ORDER - - BY - - SelectItem - - OrderSuffix - , - - -
- - -
         ::= 'AGGREGATE' SelectItem OrderSuffix? ( ',' SelectItem OrderSuffix? )* ( 'GROUP' ( 'AND' 'ORDER' )? 'BY' SelectItem OrderSuffix? ( ',' SelectItem OrderSuffix? )* )?
-
- Referenced by: -
- - -====================================================================================================================== -OrderByPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - OrderByElements - -
- - -
         ::= OrderByElements
-
- Referenced by: -
- - -====================================================================================================================== -AsPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - AS - - Alias - -
- - -
         ::= 'AS' Alias
-
- Referenced by: -
- - -====================================================================================================================== -JoinPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - JoinerExpression - -
- - -
         ::= JoinerExpression
-
- Referenced by: -
- - -====================================================================================================================== -SetPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - SET - - UpdateSets - -
- - -
         ::= 'SET' UpdateSets
-
- Referenced by: -
- - -====================================================================================================================== -DropPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - DROP - - ColumnList - -
- - -
         ::= 'DROP' ColumnList
-
- Referenced by: -
- - -====================================================================================================================== -LimitPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - LIMIT - - Expression - OFFSET - - Expression - -
- - -
         ::= 'LIMIT' Expression ( 'OFFSET' Expression )?
-
- Referenced by: -
- - -====================================================================================================================== -SetOperationModifier -====================================================================================================================== - - -.. raw:: html - - - - - - ALL - - DISTINCT - - BY - - NAME - - MATCHING - - ( - - RelObjectName - , - - ) - - STRICT - - CORRESPONDING - - ALL - - DISTINCT - - BY - - ALL - - DISTINCT - - ( - - RelObjectName - , - - ) - - ALL - - DISTINCT - - -
- - -
         ::= ( 'ALL' | 'DISTINCT' )? 'BY' 'NAME' ( 'MATCHING' '(' RelObjectName ( ',' RelObjectName )* ')' )?
-
           | 'STRICT'? 'CORRESPONDING' ( 'ALL' | 'DISTINCT' )? ( 'BY' ( 'ALL' | 'DISTINCT' - )? '(' RelObjectName ( ',' RelObjectName )* ')' )?
-
           | 'ALL'
-
           | 'DISTINCT'
-
- - -====================================================================================================================== -SetOperationPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - UNION - - INTERSECT - - EXCEPT - - SetOperationModifier - - ParenthesedSelect - , - - -
- - -
         ::= ( 'UNION' | 'INTERSECT' | 'EXCEPT' ) SetOperationModifier? ParenthesedSelect ( ',' ParenthesedSelect )*
-
- Referenced by: -
- - -====================================================================================================================== -CallPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - CALL - - TableFunction - - Alias - -
- - -
         ::= 'CALL' TableFunction Alias?
-
- Referenced by: -
- - -====================================================================================================================== -TableSamplePipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - TABLESAMPLE - - SYSTEM - - ( - - S_DOUBLE - - S_LONG - PERCENT - - ) - - -
- - -
         ::= 'TABLESAMPLE' 'SYSTEM' '(' ( S_DOUBLE | S_LONG ) 'PERCENT' ')'
-
- Referenced by: -
- - -====================================================================================================================== -PivotPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - PIVOT - - ( - - Function - FOR - - Column - IN - - ( - - SelectItemsList - ) - - ) - - Alias - -
- - -
         ::= 'PIVOT' '(' Function 'FOR' Column 'IN' '(' SelectItemsList ')' ')' Alias?
-
- Referenced by: -
- - -====================================================================================================================== -UnPivotPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - UNPIVOT - - ( - - Column - FOR - - Column - IN - - ( - - SelectItemsList - ) - - ) - - Alias - -
- - -
         ::= 'UNPIVOT' '(' Column 'FOR' Column 'IN' '(' SelectItemsList ')' ')' Alias?
-
- Referenced by: -
- - -====================================================================================================================== -TableStatement -====================================================================================================================== - - -.. raw:: html - - - - - - TABLE - - Table - - OrderByElements - - LimitWithOffset - - Offset - -
- - -
         ::= 'TABLE' Table OrderByElements? LimitWithOffset? Offset?
-
- Referenced by: -
- - -====================================================================================================================== -ParenthesedSelect -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Select - ) - - -
- - -
         ::= '(' Select ')'
-
- - -====================================================================================================================== -ParenthesedInsert -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Insert - ) - - -
- - -
         ::= '(' Insert ')'
-
- Referenced by: -
- - -====================================================================================================================== -ParenthesedUpdate -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Update - ) - - -
- - -
         ::= '(' Update ')'
-
- Referenced by: -
- - -====================================================================================================================== -ParenthesedDelete -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Delete - ) - - -
- - -
         ::= '(' Delete ')'
-
- Referenced by: -
- - -====================================================================================================================== -LateralView -====================================================================================================================== - - -.. raw:: html - - - - - - LATERAL - - VIEW - - OUTER - - Function - - RelObjectName - AS - - RelObjectName - , - - RelObjectName - -
- - -
         ::= 'LATERAL' 'VIEW' 'OUTER'? Function RelObjectName? 'AS' RelObjectName ( ',' RelObjectName )?
-
- Referenced by: -
- - -====================================================================================================================== -ForClause -====================================================================================================================== - - -.. raw:: html - - - - - - FOR - - BROWSE - - XML - - RAW - - ( - - S_CHAR_LITERAL - ) - - AUTO - - , - - BINARY - - BASE64 - - TYPE - - ROOT - - XMLSCHEMA - - ( - - S_CHAR_LITERAL - ) - - XMLDATA - - ELEMENTS - - XSINIL - - ABSENT - - EXPLICIT - - , - - BINARY - - BASE64 - - TYPE - - ROOT - - ( - - S_CHAR_LITERAL - ) - - XMLDATA - - PATH - - ( - - S_CHAR_LITERAL - ) - - , - - BINARY - - BASE64 - - TYPE - - ROOT - - ( - - S_CHAR_LITERAL - ) - - ELEMENTS - - XSINIL - - ABSENT - - JSON - - AUTO - - PATH - - , - - ROOT - - ( - - S_CHAR_LITERAL - ) - - INCLUDE_NULL_VALUES - - WITHOUT_ARRAY_WRAPPER - - -
- - -
         ::= 'FOR' ( 'BROWSE' | 'XML' ( ( 'RAW' ( '(' S_CHAR_LITERAL ')' )? | 'AUTO' ) ( ',' ( 'BINARY' 'BASE64' | 'TYPE' | ( 'ROOT' | 'XMLSCHEMA' ) ( - '(' S_CHAR_LITERAL ')' )? | 'XMLDATA' | 'ELEMENTS' ( 'XSINIL' | 'ABSENT' )? ) )* | 'EXPLICIT' ( ',' - ( 'BINARY' 'BASE64' | 'TYPE' | 'ROOT' ( '(' S_CHAR_LITERAL ')' )? | 'XMLDATA' ) )* | 'PATH' ( '(' S_CHAR_LITERAL ')' )? ( ',' ( 'BINARY' 'BASE64' | 'TYPE' | 'ROOT' ( '(' S_CHAR_LITERAL ')' )? | 'ELEMENTS' ( 'XSINIL' | 'ABSENT' )? ) )* ) | 'JSON' ( 'AUTO' | 'PATH' ) - ( ',' ( 'ROOT' ( '(' S_CHAR_LITERAL ')' )? | 'INCLUDE_NULL_VALUES' | 'WITHOUT_ARRAY_WRAPPER' ) )* )
-
- Referenced by: -
- - -====================================================================================================================== -LateralViews -====================================================================================================================== - - -.. raw:: html - - - - - - LateralView - -
- - -
         ::= LateralView+
-
- Referenced by: -
- - -====================================================================================================================== -LateralSubSelect -====================================================================================================================== - - -.. raw:: html - - - - - - LATERAL - - ( - - Select - ) - - -
- - -
         ::= 'LATERAL' '(' Select ')'
-
- Referenced by: -
- - -====================================================================================================================== -PlainSelect -====================================================================================================================== - - -.. raw:: html - - - - - - K_SELECT - STRAIGHT_JOIN - - Skip - - First - - Top - ALL - - DISTINCT - - ON - - ( - - SelectItemsList - ) - - DISTINCTROW - - UNIQUE - - SQL_CALC_FOUND_ROWS - - SQL_NO_CACHE - - SQL_CACHE - - AS - - STRUCT - - VALUE - - Top - - SelectItemsList - - IntoClause - FROM - - FromItem - - LateralViews - - JoinsList - FROM - - ONLY - - FromItem - - LateralViews - - JoinsList - FINAL - - KSQLWindowClause - - PreWhereClause - - WhereClause - - OracleHierarchicalQueryClause - - PreferringClause - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - Having - - GroupByColumnReferences - - Having - - Qualify - - OrderByElements - WINDOW - - RelObjectName - AS - - windowDefinition - , - - OrderByElements - - ForClause - EMIT - - CHANGES - - LimitBy - - LimitWithOffset - - Offset - - LimitWithOffset - - Fetch - - WithIsolation - FOR - - NO - - KEY - - UPDATE - - KEY - - SHARE - - READ - - FETCH - - ONLY - - OF - - Table - - Wait - NOWAIT - - SKIP - - LOCKED - - SETTINGS - - UpdateSets - - OptimizeFor - INTO - - TEMP - - Table - WITH - - NO - - LOG - - -
- - -
         ::= K_SELECT 'STRAIGHT_JOIN'? Skip? First? Top? ( 'ALL' | 'DISTINCT' ( 'ON' '(' SelectItemsList ')' )? | 'DISTINCTROW' | 'UNIQUE' | 'SQL_CALC_FOUND_ROWS' | 'SQL_NO_CACHE' | 'SQL_CACHE' - )? ( 'AS' ( 'STRUCT' | 'VALUE' ) )? Top? SelectItemsList IntoClause? ( 'FROM' FromItem LateralViews? JoinsList? )? ( 'FROM' 'ONLY' FromItem LateralViews? JoinsList? )? 'FINAL'? KSQLWindowClause? PreWhereClause? WhereClause? OracleHierarchicalQueryClause? ( PreferringClause ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? )? Having? GroupByColumnReferences? Having? Qualify? OrderByElements? ( 'WINDOW' RelObjectName 'AS' windowDefinition ( ',' RelObjectName 'AS' windowDefinition )* )? OrderByElements? ForClause? ( 'EMIT' 'CHANGES' )? LimitBy? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation? ( 'FOR' ( ( 'NO' 'KEY' )? 'UPDATE' | 'KEY'? 'SHARE' | ( 'READ' | 'FETCH' ) 'ONLY' - ) ( 'OF' Table )? Wait? ( 'NOWAIT' | 'SKIP' 'LOCKED' )? )? ( 'SETTINGS' UpdateSets )? OptimizeFor? ( 'INTO' 'TEMP' Table )? ( 'WITH' 'NO' 'LOG' )?
-
- Referenced by: -
- - -====================================================================================================================== -SetOperationList -====================================================================================================================== - - -.. raw:: html - - - - - - UNION - - INTERSECT - - MINUS - - EXCEPT - - SetOperationModifier - - PlainSelect - - Values - - ParenthesedSelect - - OrderByElements - - LimitWithOffset - - Offset - - LimitWithOffset - - Fetch - - WithIsolation - -
- - -
         ::= ( ( 'UNION' | 'INTERSECT' | 'MINUS' | 'EXCEPT' ) SetOperationModifier? ( PlainSelect | Values | ParenthesedSelect ) )+ OrderByElements? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation?
-
- Referenced by: -
- - -====================================================================================================================== -WithList -====================================================================================================================== - - -.. raw:: html - - - - - - WITH - - WithItem - , - - -
- -
WithList ::= 'WITH' WithItem ( ',' WithItem )*
-
- Referenced by: -
- - -====================================================================================================================== -WithItem -====================================================================================================================== - - -.. raw:: html - - - - - - FUNCTION - - WithFunctionDeclaration - RECURSIVE - - RelObjectName - ( - - SelectItemsList - ) - - AS - - NOT - - MATERIALIZED - - ParenthesedSelect - - ParenthesedInsert - - ParenthesedUpdate - - ParenthesedDelete - - WithSearchClause - -
- -
WithItem ::= ( 'FUNCTION' WithFunctionDeclaration | 'RECURSIVE'? RelObjectName ( '(' SelectItemsList ')' )? 'AS' ( 'NOT'? 'MATERIALIZED' )? ( ParenthesedSelect | ParenthesedInsert | ParenthesedUpdate | ParenthesedDelete ) ) WithSearchClause?
-
- Referenced by: -
- - -====================================================================================================================== -WithSearchClause -====================================================================================================================== - - -.. raw:: html - - - - - - SEARCH - - BREADTH - - DEPTH - - FIRST - - BY - - Column - , - - SET - - RelObjectName - -
- - -
         ::= 'SEARCH' ( 'BREADTH' | 'DEPTH' ) 'FIRST' 'BY' Column ( ',' Column )* 'SET' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -WithFunctionDeclaration -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ( - - WithFunctionParameter - , - - ) - - RETURNS - - RelObjectName - RETURN - - Expression - -
- - -
         ::= RelObjectName '(' ( WithFunctionParameter ( ',' WithFunctionParameter )* )? ')' 'RETURNS' RelObjectName 'RETURN' Expression
-
- Referenced by: -
- - -====================================================================================================================== -WithFunctionParameter -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ARRAY - - < - - RelObjectName - > - - RelObjectName - -
- - -
         ::= RelObjectName ( 'ARRAY' '<' RelObjectName '>' | RelObjectName )
-
- Referenced by: -
- - -====================================================================================================================== -ColumnSelectItemsList -====================================================================================================================== - - -.. raw:: html - - - - - - SelectItem - , - - -
- - -
         ::= SelectItem ( ',' SelectItem )*
-
- Referenced by: -
- - -====================================================================================================================== -SelectItemsList -====================================================================================================================== - - -.. raw:: html - - - - - - SelectItem - , - - -
- - -
         ::= SelectItem ( ',' SelectItem )*
-
- - -====================================================================================================================== -FunctionAllColumns -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Function - ) - - . - - * - - -
- - -
         ::= '('+ Function ')'+ '.' '*'
-
- Not referenced by any. -
- - -====================================================================================================================== -SelectItem -====================================================================================================================== - - -.. raw:: html - - - - - - ConnectByPriorOperator - - XorExpression - - ConcatExpression - - Expression - - Alias - -
- - - -
- - -====================================================================================================================== -AllColumns -====================================================================================================================== - - -.. raw:: html - - - - - - * - - EXCEPT - - EXCLUDE - - ParenthesedColumnList - REPLACE - - ( - - SelectItemsList - ) - - -
- - -
         ::= '*' ( ( 'EXCEPT' | 'EXCLUDE' ) ParenthesedColumnList )? ( 'REPLACE' '(' SelectItemsList ')' )?
-
- - -====================================================================================================================== -AllTableColumns -====================================================================================================================== - - -.. raw:: html - - - - - - Table - . - - AllColumns - -
- - -
         ::= Table '.' AllColumns
-
- - -====================================================================================================================== -Alias -====================================================================================================================== - - -.. raw:: html - - - - - - AS - - RelObjectName - ( - - RelObjectName - - ColDataType - , - - ) - - AS - - RelObjectName - - S_CHAR_LITERAL - ( - - RelObjectName - - ColDataType - , - - ) - - -
- - -
           | 'AS'? ( RelObjectName | S_CHAR_LITERAL ) ( '(' RelObjectName ColDataType? ( ',' RelObjectName ColDataType? )* ')' )?
-
- - -====================================================================================================================== -SQLServerHint -====================================================================================================================== - - -.. raw:: html - - - - - - INDEX - - ( - - RelObjectName - ) - - NOLOCK - - -
- - -
         ::= 'INDEX' '(' RelObjectName ')'
-
           | 'NOLOCK'
-
- Referenced by: -
- - -====================================================================================================================== -SQLServerHints -====================================================================================================================== - - -.. raw:: html - - - - - - WITH - - ( - - SQLServerHint - , - - ) - - -
- - -
         ::= 'WITH' '(' SQLServerHint ( ',' SQLServerHint )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -MySQLIndexHint -====================================================================================================================== - - -.. raw:: html - - - - - - USE - - SHOW - - IGNORE - - FORCE - - INDEX - - KEY - - ( - - RelObjectName - , - - ) - - -
- - -
         ::= ( 'USE' | 'SHOW' | 'IGNORE' | 'FORCE' ) ( 'INDEX' | 'KEY' ) '(' RelObjectName ( ',' RelObjectName )* ')'
-
- - -====================================================================================================================== -FunctionItem -====================================================================================================================== - - -.. raw:: html - - - - - - Function - - Alias - -
- - -
         ::= Function Alias?
-
- Referenced by: -
- - -====================================================================================================================== -PivotForColumns -====================================================================================================================== - - -.. raw:: html - - - - - - ParenthesedColumnList - - Column - -
- - -
         ::= ParenthesedColumnList
-
           | Column
-
- Referenced by: -
- - -====================================================================================================================== -PivotFunctionItems -====================================================================================================================== - - -.. raw:: html - - - - - - FunctionItem - , - - -
- - -
         ::= FunctionItem ( ',' FunctionItem )*
-
- Referenced by: -
- - -====================================================================================================================== -ExpressionListItem -====================================================================================================================== - - -.. raw:: html - - - - - - ParenthesedExpressionList - - Alias - -
- - -
         ::= ParenthesedExpressionList Alias?
-
- Referenced by: -
- - -====================================================================================================================== -PivotMultiInItems -====================================================================================================================== - - -.. raw:: html - - - - - - ExpressionListItem - , - - -
- - -
         ::= ExpressionListItem ( ',' ExpressionListItem )*
-
- Referenced by: -
- - -====================================================================================================================== -Pivot -====================================================================================================================== - - -.. raw:: html - - - - - - PIVOT - - ( - - PivotFunctionItems - FOR - - PivotForColumns - IN - - ( - - SelectItemsList - - PivotMultiInItems - ) - - ) - - Alias - -
- -
Pivot    ::= 'PIVOT' '(' PivotFunctionItems 'FOR' PivotForColumns 'IN' '(' ( SelectItemsList | PivotMultiInItems ) ')' ')' Alias?
-
- Referenced by: -
- - -====================================================================================================================== -PivotXml -====================================================================================================================== - - -.. raw:: html - - - - - - PIVOT - - XML - - ( - - PivotFunctionItems - FOR - - PivotForColumns - IN - - ( - - ANY - - Select - - SelectItemsList - - PivotMultiInItems - ) - - ) - - -
- -
PivotXml ::= 'PIVOT' 'XML' '(' PivotFunctionItems 'FOR' PivotForColumns 'IN' '(' ( 'ANY' | Select | SelectItemsList | PivotMultiInItems ) ')' ')'
-
- Referenced by: -
- - -====================================================================================================================== -UnPivot -====================================================================================================================== - - -.. raw:: html - - - - - - UNPIVOT - - INCLUDE - - EXCLUDE - - NULLS - - ( - - PivotForColumns - FOR - - PivotForColumns - IN - - ( - - SelectItemsList - ) - - ) - - Alias - -
- -
UnPivot  ::= 'UNPIVOT' ( ( 'INCLUDE' | 'EXCLUDE' ) 'NULLS' )? '(' PivotForColumns 'FOR' PivotForColumns 'IN' '(' SelectItemsList ')' ')' Alias?
-
- Referenced by: -
- - -====================================================================================================================== -IntoClause -====================================================================================================================== - - -.. raw:: html - - - - - - INTO - - Table - , - - -
- - -
         ::= 'INTO' Table ( ',' Table )*
-
- Referenced by: -
- - -====================================================================================================================== -ParenthesedFromItem -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - FromItem - - JoinsList - ) - - -
- - -
         ::= '(' FromItem JoinsList? ')'
-
- Referenced by: -
- - -====================================================================================================================== -FromItem -====================================================================================================================== - - -.. raw:: html - - - - - - Values - - TableFunction - - Table - - ParenthesedFromItem - - ParenthesedSelect - - Pivot - - UnPivot - - LateralSubSelect - - SubImport - - Select - - Alias - - TimeTravelAfterAlias - - SampleClause - - UnPivot - - PivotXml - - Pivot - - MySQLIndexHint - - SQLServerHints - -
- - -
- - -====================================================================================================================== -JoinsList -====================================================================================================================== - - -.. raw:: html - - - - - - JoinerExpression - -
- - -
         ::= JoinerExpression+
-
- - -====================================================================================================================== -JoinHint -====================================================================================================================== - - -.. raw:: html - - - - - - LOOP - - HASH - - MERGE - - REMOTE - - -
- -
JoinHint ::= 'LOOP'
-
           | 'HASH'
-
           | 'MERGE'
-
           | 'REMOTE'
-
- Referenced by: -
- - -====================================================================================================================== -JoinerExpression -====================================================================================================================== - - -.. raw:: html - - - - - - GLOBAL - - ANY - - ALL - - NATURAL - - LEFT - - SEMI - - OUTER - - ANY - - ALL - - RIGHT - - FULL - - OUTER - - ANY - - ALL - - INNER - - CROSS - - OUTER - - JoinHint - JOIN - - FETCH - - , - - OUTER - - STRAIGHT_JOIN - - APPLY - - FromItem - WITHIN - - ( - - JoinWindow - ) - - ON - - Expression - USING - - ( - - Column - , - - ) - - -
- - -
         ::= 'GLOBAL'? ( 'ANY' | 'ALL' )? 'NATURAL'? ( 'LEFT' ( 'SEMI' | 'OUTER' | - 'ANY' | 'ALL' )? | ( 'RIGHT' | 'FULL' ) ( 'OUTER' | 'ANY' | 'ALL' )? | 'INNER' | 'CROSS' - | 'OUTER' )? ( JoinHint? 'JOIN' 'FETCH'? | ',' 'OUTER'? | 'STRAIGHT_JOIN' | 'APPLY' ) FromItem ( ( 'WITHIN' '(' JoinWindow ')' )? ( 'ON' Expression )+ | 'USING' '(' Column ( ',' Column )* ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -JoinWindow -====================================================================================================================== - - -.. raw:: html - - - - - - S_LONG - - S_IDENTIFIER - - K_DATE_LITERAL - , - - S_LONG - - S_IDENTIFIER - - K_DATE_LITERAL - -
- - -
         ::= S_LONG ( S_IDENTIFIER | K_DATE_LITERAL ) ( ',' S_LONG ( S_IDENTIFIER | K_DATE_LITERAL ) )?
-
- Referenced by: -
- - -====================================================================================================================== -KSQLWindowClause -====================================================================================================================== - - -.. raw:: html - - - - - - WINDOW - - HOPPING - - ( - - SIZE - - S_LONG - - S_IDENTIFIER - , - - ADVANCE - - BY - - SESSION - - ( - - TUMBLING - - ( - - SIZE - - S_LONG - - S_IDENTIFIER - ) - - -
- - -
         ::= 'WINDOW' ( 'HOPPING' '(' 'SIZE' S_LONG S_IDENTIFIER ',' 'ADVANCE' 'BY' | 'SESSION' '(' | 'TUMBLING' '(' 'SIZE' ) S_LONG S_IDENTIFIER ')'
-
- Referenced by: -
- - -====================================================================================================================== -WhereClause -====================================================================================================================== - - -.. raw:: html - - - - - - WHERE - - Expression - -
- - -
         ::= 'WHERE' Expression
-
- - -====================================================================================================================== -PreWhereClause -====================================================================================================================== - - -.. raw:: html - - - - - - PREWHERE - - Expression - -
- - -
         ::= 'PREWHERE' Expression
-
- Referenced by: -
- - -====================================================================================================================== -OracleHierarchicalQueryClause -====================================================================================================================== - - -.. raw:: html - - - - - - START - - WITH - - XorExpression - CONNECT - - BY - - NOCYCLE - - CONNECT - - BY - - NOCYCLE - - XorExpression - START - - WITH - - XorExpression - -
- - -
         ::= ( 'START' 'WITH' XorExpression 'CONNECT' 'BY' 'NOCYCLE'? | 'CONNECT' 'BY' 'NOCYCLE'? ( XorExpression 'START' 'WITH' )? ) XorExpression
-
- Referenced by: -
- - -====================================================================================================================== -PreferringClause -====================================================================================================================== - - -.. raw:: html - - - - - - PREFERRING - - PreferenceTerm - -
- - -
         ::= 'PREFERRING' PreferenceTerm
-
- Referenced by: -
- - -====================================================================================================================== -PreferenceTerm -====================================================================================================================== - - -.. raw:: html - - - - - - Plus - -
- - -
         ::= Plus
-
- Referenced by: -
- - -====================================================================================================================== -Plus -====================================================================================================================== - - -.. raw:: html - - - - - - PriorTo - PLUS - - -
- -
Plus     ::= PriorTo ( 'PLUS' PriorTo )*
-
- Referenced by: -
- - -====================================================================================================================== -PriorTo -====================================================================================================================== - - -.. raw:: html - - - - - - PreferenceTermTerminal - ( - - PreferenceTerm - ) - - TO - - PRIOR - - -
- -
PriorTo  ::= ( PreferenceTermTerminal | '(' PreferenceTerm ')' ) ( 'PRIOR' 'TO' ( PreferenceTermTerminal | '(' PreferenceTerm ')' ) )*
-
- Referenced by: -
- - -====================================================================================================================== -PreferenceTermTerminal -====================================================================================================================== - - -.. raw:: html - - - - - - HighExpression - - LowExpression - - Inverse - - Condition - -
- - -
         ::= HighExpression
-
           | LowExpression
-
           | Inverse
-
           | Condition
-
- Referenced by: -
- - -====================================================================================================================== -HighExpression -====================================================================================================================== - - -.. raw:: html - - - - - - HIGH - - Expression - -
- - -
         ::= 'HIGH' Expression
-
- Referenced by: -
- - -====================================================================================================================== -LowExpression -====================================================================================================================== - - -.. raw:: html - - - - - - LOW - - Expression - -
- - -
         ::= 'LOW' Expression
-
- Referenced by: -
- - -====================================================================================================================== -Inverse -====================================================================================================================== - - -.. raw:: html - - - - - - INVERSE - - ( - - PreferenceTerm - ) - - -
- -
Inverse  ::= 'INVERSE' '(' PreferenceTerm ')'
-
- Referenced by: -
- - -====================================================================================================================== -GroupByColumnReferences -====================================================================================================================== - - -.. raw:: html - - - - - - GROUP - - BY - - GROUPING - - SETS - - ( - - GroupingSet - , - - ) - - ExpressionList - GROUPING - - SETS - - ( - - GroupingSet - , - - ) - - WITH - - ROLLUP - - -
- - -
         ::= 'GROUP' 'BY' ( 'GROUPING' 'SETS' '(' GroupingSet ( ',' GroupingSet )* ')' | ExpressionList ( 'GROUPING' 'SETS' '(' GroupingSet ( ',' GroupingSet )* ')' )? ( 'WITH' 'ROLLUP' )? )
-
- Referenced by: -
- - -====================================================================================================================== -GroupingSet -====================================================================================================================== - - -.. raw:: html - - - - - - ParenthesedExpressionList - - SimpleExpression - -
- - -
         ::= ParenthesedExpressionList
-
           | SimpleExpression
-
- Referenced by: -
- - -====================================================================================================================== -Having -====================================================================================================================== - - -.. raw:: html - - - - - - HAVING - - Expression - -
- -
Having   ::= 'HAVING' Expression
-
- Referenced by: -
- - -====================================================================================================================== -Qualify -====================================================================================================================== - - -.. raw:: html - - - - - - QUALIFY - - Expression - -
- -
Qualify  ::= 'QUALIFY' Expression
-
- Referenced by: -
- - -====================================================================================================================== -OrderByElements -====================================================================================================================== - - -.. raw:: html - - - - - - ORDER - - SIBLINGS - - BY - - OrderByElement - , - - -
- - -
         ::= 'ORDER' 'SIBLINGS'? 'BY' OrderByElement ( ',' OrderByElement )*
-
- - -====================================================================================================================== -OrderByElement -====================================================================================================================== - - -.. raw:: html - - - - - - Expression - COLLATE - - S_CHAR_LITERAL - - S_QUOTED_IDENTIFIER - ASC - - DESC - - NULLS - - FIRST - - LAST - - WITH - - ROLLUP - - -
- - -
         ::= Expression ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER ) )? ( 'ASC' | 'DESC' )? ( 'NULLS' ( 'FIRST' | 'LAST' )? )? ( 'WITH' 'ROLLUP' )?
-
- Referenced by: -
- - -====================================================================================================================== -JdbcParameter -====================================================================================================================== - - -.. raw:: html - - - - - - ? - - S_PARAMETER - - S_LONG - -
- - -
         ::= ( '?' | S_PARAMETER ) S_LONG?
-
- - -====================================================================================================================== -LimitWithOffset -====================================================================================================================== - - -.. raw:: html - - - - - - LIMIT - - ParenthesedSelect - - Expression - , - - Expression - -
- - -
         ::= 'LIMIT' ( ParenthesedSelect | Expression ) ( ',' Expression )?
-
- - -====================================================================================================================== -PlainLimit -====================================================================================================================== - - -.. raw:: html - - - - - - LIMIT - - ParenthesedSelect - - Expression - -
- - -
         ::= 'LIMIT' ( ParenthesedSelect | Expression )
-
- Referenced by: -
- - -====================================================================================================================== -LimitBy -====================================================================================================================== - - -.. raw:: html - - - - - - LimitWithOffset - BY - - ExpressionList - -
- - -
- Referenced by: -
- - -====================================================================================================================== -Offset -====================================================================================================================== - - -.. raw:: html - - - - - - OFFSET - - Expression - ROWS - - ROW - - -
- -
Offset   ::= 'OFFSET' Expression ( 'ROWS' | 'ROW' )?
-
- - -====================================================================================================================== -Fetch -====================================================================================================================== - - -.. raw:: html - - - - - - FETCH - - FIRST - - NEXT - - Expression - PERCENT - - ROWS - - ROW - - ONLY - - WITH TIES - - -
- -
Fetch    ::= 'FETCH' ( 'FIRST' | 'NEXT' ) ( Expression 'PERCENT'? )? ( 'ROWS' | 'ROW' ) ( 'ONLY' | 'WITH TIES' )
-
- - -====================================================================================================================== -WithIsolation -====================================================================================================================== - - -.. raw:: html - - - - - - WITH - - K_ISOLATION - -
- - -
         ::= 'WITH' K_ISOLATION
-
- - -====================================================================================================================== -OptimizeFor -====================================================================================================================== - - -.. raw:: html - - - - - - OPTIMIZE - - FOR - - S_LONG - ROWS - - -
- - -
         ::= 'OPTIMIZE' 'FOR' S_LONG 'ROWS'
-
- Referenced by: -
- - -====================================================================================================================== -Top -====================================================================================================================== - - -.. raw:: html - - - - - - TOP - - S_LONG - - JdbcParameter - : - - S_IDENTIFIER - ( - - AdditiveExpression - ) - - PERCENT - - WITH TIES - - -
- -
Top      ::= 'TOP' ( S_LONG | JdbcParameter | ':' S_IDENTIFIER? | '(' AdditiveExpression ')' ) 'PERCENT'? 'WITH TIES'?
-
- Referenced by: -
- - -====================================================================================================================== -Skip -====================================================================================================================== - - -.. raw:: html - - - - - - SKIP - - S_LONG - - S_IDENTIFIER - - JdbcParameter - -
- -
Skip     ::= 'SKIP' ( S_LONG | S_IDENTIFIER | JdbcParameter )
-
- Referenced by: -
- - -====================================================================================================================== -First -====================================================================================================================== - - -.. raw:: html - - - - - - FIRST - - LIMIT - - S_LONG - - S_IDENTIFIER - - JdbcParameter - -
- -
First    ::= ( 'FIRST' | 'LIMIT' ) ( S_LONG | S_IDENTIFIER | JdbcParameter )
-
- Referenced by: -
- - -====================================================================================================================== -Expression -====================================================================================================================== - - -.. raw:: html - - - - - - XorExpression - -
- - -
         ::= XorExpression
-
- - -====================================================================================================================== -XorExpression -====================================================================================================================== - - -.. raw:: html - - - - - - OrExpression - XOR - - -
- - -
         ::= OrExpression ( 'XOR' OrExpression )*
-
- - -====================================================================================================================== -OrExpression -====================================================================================================================== - - -.. raw:: html - - - - - - AndExpression - OR - - -
- - -
         ::= AndExpression ( 'OR' AndExpression )*
-
- Referenced by: -
- - -====================================================================================================================== -AndExpression -====================================================================================================================== - - -.. raw:: html - - - - - - Condition - NOT - - ! - - ( - - XorExpression - ) - - AND - - && - - -
- - -
         ::= ( Condition | ( 'NOT' | '!' )? '(' XorExpression ')' ) ( ( 'AND' | '&&' ) ( Condition | ( 'NOT' | '!' )? '(' XorExpression ')' ) )*
-
- Referenced by: -
- - -====================================================================================================================== -Condition -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - ! - - ExistsExpression - PRIOR - - SimpleExpression - ( - - + - - ) - - RegularConditionRHS - - OverlapsCondition - - InExpression - - ExcludesExpression - - IncludesExpression - - Between - - MemberOfExpression - - IsNullExpression - - IsBooleanExpression - - IsUnknownExpression - - LikeExpression - - IsDistinctExpression - - SimilarToExpression - -
- - - -
- - -====================================================================================================================== -RegularConditionRHS -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - + - - ) - - > - - < - - = - - OP_GREATERTHANEQUALS - - OP_MINORTHANEQUALS - - OP_NOTEQUALSSTANDARD - - OP_NOTEQUALSBANG - - OP_NOTEQUALSHAT - *= - - =* - - && - - &> - - <& - - @@ - - ~ - - ~* - - !~ - - !~* - - @> - - <@ - - ? - - ?| - - ?& - - OP_CONCAT - - - - -# - - <-> - - <#> - - <=> - - PRIOR - - ComparisonItem - ( - - + - - ) - - -
- - -
         ::= ( '(' '+' ')' )? ( '>' | '<' | '=' | OP_GREATERTHANEQUALS | OP_MINORTHANEQUALS | OP_NOTEQUALSSTANDARD | OP_NOTEQUALSBANG | OP_NOTEQUALSHAT | '*=' | '=*' | '&&' | '&>' | '<&' | '@@' | '~' | '~*' | '!~' | '!~*' | '@>' | '<@' - | '?' | '?|' | '?&' | OP_CONCAT | '-' | '-#' | '<->' | '<#>' | '<=>' ) 'PRIOR'? ComparisonItem ( '(' '+' ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -OverlapsCondition -====================================================================================================================== - - -.. raw:: html - - - - - - OVERLAPS - - ParenthesedExpressionList - -
- - -
         ::= 'OVERLAPS' ParenthesedExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -SQLCondition -====================================================================================================================== - - -.. raw:: html - - - - - - ExistsExpression - - SimpleExpression - - OverlapsCondition - - InExpression - - ExcludesExpression - - IncludesExpression - - Between - - MemberOfExpression - - IsNullExpression - - IsBooleanExpression - - IsUnknownExpression - - LikeExpression - - IsDistinctExpression - - SimilarToExpression - -
- - -
         ::= ExistsExpression
-
-
- Not referenced by any. -
- - -====================================================================================================================== -InExpression -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - + - - ) - - GLOBAL - - NOT - - IN - - S_CHAR_LITERAL - - PrimaryExpression - -
- - -
         ::= ( '(' '+' ')' )? 'GLOBAL'? 'NOT'? 'IN' ( S_CHAR_LITERAL | PrimaryExpression )
-
- Referenced by: -
- - -====================================================================================================================== -IncludesExpression -====================================================================================================================== - - -.. raw:: html - - - - - - INCLUDES - - ParenthesedExpressionList - -
- - -
         ::= 'INCLUDES' ParenthesedExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -ExcludesExpression -====================================================================================================================== - - -.. raw:: html - - - - - - EXCLUDES - - ParenthesedExpressionList - -
- - -
         ::= 'EXCLUDES' ParenthesedExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -Between -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - BETWEEN - - SYMMETRIC - - ASYMMETRIC - - ParenthesedSelect - - SimpleExpression - - RegularConditionRHS - AND - - ParenthesedSelect - - SimpleExpression - - RegularConditionRHS - -
- -
Between  ::= 'NOT'? 'BETWEEN' ( 'SYMMETRIC' | 'ASYMMETRIC' )? ( ParenthesedSelect | SimpleExpression RegularConditionRHS? ) 'AND' ( ParenthesedSelect | SimpleExpression RegularConditionRHS? )
-
- Referenced by: -
- - -====================================================================================================================== -LikeExpression -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - LIKE - - ILIKE - - RLIKE - - REGEXP_LIKE - - REGEXP - - K_SIMILAR_TO - MATCH_ANY - - MATCH_ALL - - MATCH_PHRASE - - MATCH_PHRASE_PREFIX - - MATCH_REGEXP - - BINARY - - SimpleExpression - ESCAPE - - S_CHAR_LITERAL - - Expression - -
- - -
         ::= 'NOT'? ( 'LIKE' | 'ILIKE' | 'RLIKE' | 'REGEXP_LIKE' | 'REGEXP' | K_SIMILAR_TO | 'MATCH_ANY' | 'MATCH_ALL' | 'MATCH_PHRASE' | 'MATCH_PHRASE_PREFIX' | 'MATCH_REGEXP' - ) 'BINARY'? SimpleExpression ( 'ESCAPE' ( S_CHAR_LITERAL | Expression ) )?
-
- Referenced by: -
- - -====================================================================================================================== -SimilarToExpression -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - SIMILAR - - TO - - SimpleExpression - ESCAPE - - S_CHAR_LITERAL - -
- - -
         ::= 'NOT'? 'SIMILAR' 'TO' SimpleExpression ( 'ESCAPE' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -IsDistinctExpression -====================================================================================================================== - - -.. raw:: html - - - - - - IS - - NOT - - DISTINCT - - FROM - - SimpleExpression - -
- - -
         ::= 'IS' 'NOT'? 'DISTINCT' 'FROM' SimpleExpression
-
- Referenced by: -
- - -====================================================================================================================== -IsNullExpression -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - ISNULL - - NOTNULL - - IS - - NOT - - NULL - - -
- - -
         ::= 'NOT'? 'ISNULL'
-
           | 'NOTNULL'
-
           | 'IS' 'NOT'? 'NULL'
-
- Referenced by: -
- - -====================================================================================================================== -IsBooleanExpression -====================================================================================================================== - - -.. raw:: html - - - - - - IS - - NOT - - TRUE - - FALSE - - -
- - -
         ::= 'IS' 'NOT'? ( 'TRUE' | 'FALSE' )
-
- Referenced by: -
- - -====================================================================================================================== -IsUnknownExpression -====================================================================================================================== - - -.. raw:: html - - - - - - IS - - NOT - - UNKNOWN - - -
- - -
         ::= 'IS' 'NOT'? 'UNKNOWN'
-
- Referenced by: -
- - -====================================================================================================================== -ExistsExpression -====================================================================================================================== - - -.. raw:: html - - - - - - EXISTS - - SimpleExpression - -
- - -
         ::= 'EXISTS' SimpleExpression
-
- Referenced by: -
- - -====================================================================================================================== -MemberOfExpression -====================================================================================================================== - - -.. raw:: html - - - - - - MEMBER - - OF - - Expression - -
- - -
         ::= 'MEMBER' 'OF' Expression
-
- Referenced by: -
- - -====================================================================================================================== -ExpressionList -====================================================================================================================== - - -.. raw:: html - - - - - - ComplexExpressionList - - SimpleExpressionList - - ParenthesedExpressionList - -
- - -
         ::= ComplexExpressionList
-
           | SimpleExpressionList
-
           | ParenthesedExpressionList
-
- - -====================================================================================================================== -ParenthesedExpressionList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - ComplexExpressionList - - SimpleExpressionList - ) - - -
- - -
         ::= '(' ( ComplexExpressionList | SimpleExpressionList )? ')'
-
- - -====================================================================================================================== -SimpleExpressionList -====================================================================================================================== - - -.. raw:: html - - - - - - SimpleExpression - , - - LambdaExpression - - SimpleExpression - -
- - -
         ::= SimpleExpression ( ',' ( LambdaExpression | SimpleExpression ) )*
-
- - -====================================================================================================================== -ColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - Column - , - - -
- - -
         ::= Column ( ',' Column )*
-
- - -====================================================================================================================== -ParenthesedColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - ColumnList - ) - - -
- - -
         ::= '(' ColumnList ')'
-
- - -====================================================================================================================== -ComplexExpressionList -====================================================================================================================== - - -.. raw:: html - - - - - - OracleNamedFunctionParameter - - PostgresNamedFunctionParameter - - Expression - , - - OracleNamedFunctionParameter - - PostgresNamedFunctionParameter - - LambdaExpression - - Expression - -
- - - -
- - -====================================================================================================================== -NamedExpressionListExprFirst -====================================================================================================================== - - -.. raw:: html - - - - - - SimpleExpression - FROM - - IN - - PLACING - - SimpleExpression - FOR - - FROM - - SimpleExpression - FOR - - SimpleExpression - -
- - -
         ::= SimpleExpression ( 'FROM' | 'IN' | 'PLACING' ) SimpleExpression ( ( 'FOR' | 'FROM' ) SimpleExpression ( 'FOR' SimpleExpression )? )?
-
- - -====================================================================================================================== -ComparisonItem -====================================================================================================================== - - -.. raw:: html - - - - - - AnyComparisonExpression - - SimpleExpression - - ParenthesedExpressionList - - RowConstructor - - PrimaryExpression - -
- - -
         ::= AnyComparisonExpression
-
           | SimpleExpression
-
           | ParenthesedExpressionList
-
           | RowConstructor
-
           | PrimaryExpression
-
- Referenced by: -
- - -====================================================================================================================== -AnyComparisonExpression -====================================================================================================================== - - -.. raw:: html - - - - - - ANY - - SOME - - ALL - - ParenthesedSelect - -
- - -
         ::= ( 'ANY' | 'SOME' | 'ALL' ) ParenthesedSelect
-
- Referenced by: -
- - -====================================================================================================================== -SimpleExpression -====================================================================================================================== - - -.. raw:: html - - - - - - UserVariable - = - - := - - ConcatExpression - -
- - -
         ::= ( UserVariable ( '=' | ':=' ) )? ConcatExpression
-
- - -====================================================================================================================== -ConcatExpression -====================================================================================================================== - - -.. raw:: html - - - - - - BitwiseAndOr - - OP_CONCAT - -
- - -
         ::= BitwiseAndOr ( OP_CONCAT BitwiseAndOr )*
-
- Referenced by: -
- - -====================================================================================================================== -BitwiseAndOr -====================================================================================================================== - - -.. raw:: html - - - - - - AdditiveExpression - | - - & - - << - - >> - - -
- - -
         ::= AdditiveExpression ( ( '|' | '&' | '<<' | '>>' ) AdditiveExpression )*
-
- Referenced by: -
- - -====================================================================================================================== -AdditiveExpression -====================================================================================================================== - - -.. raw:: html - - - - - - MultiplicativeExpression - + - - - - - -
- - -
         ::= MultiplicativeExpression ( ( '+' | '-' ) MultiplicativeExpression )*
-
- Referenced by: -
- - -====================================================================================================================== -MultiplicativeExpression -====================================================================================================================== - - -.. raw:: html - - - - - - BitwiseXor - * - - / - - DIV - - % - - -
- - -
         ::= BitwiseXor ( ( '*' | '/' | 'DIV' | '%' ) BitwiseXor )*
-
- Referenced by: -
- - -====================================================================================================================== -BitwiseXor -====================================================================================================================== - - -.. raw:: html - - - - - - PrimaryExpression - ^ - - -
- - -
         ::= PrimaryExpression ( '^' PrimaryExpression )*
-
- Referenced by: -
- - -====================================================================================================================== -ArrayExpression -====================================================================================================================== - - -.. raw:: html - - - - - - [ - - SimpleExpression - : - - SimpleExpression - ] - - -
- - -
         ::= ( '[' SimpleExpression? ( ':' SimpleExpression? )? ']' )+
-
- Referenced by: -
- - -====================================================================================================================== -PrimaryExpression -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - ! - - + - - - - - ~ - - NULL - - CaseWhenExpression - - CharacterPrimary - - ImplicitCast - - JdbcParameter - - JdbcNamedParameter - - UserVariable - - NumericBind - - ExtractExpression - - XMLSerializeExpr - - JsonFunction - - JsonAggregateFunction - - FullTextSearch - - CastExpression - - Function - - AnalyticExpression - - DateUnitExpression - - IntervalExpression - - S_DOUBLE - - S_LONG - - S_HEX - - AllColumns - - AllTableColumns - - K_TIME_KEY_EXPR - CURRENT - - DateTimeLiteralExpression - - StructType - ARRAY - - < - - ColDataType - > - - ArrayConstructor - - NextValExpression - - ConnectByRootOperator - - ConnectByPriorOperator - ALL - - Column - ( - - + - - ) - - TRUE - - FALSE - - S_CHAR_LITERAL - {d - - {t - - {ts - - S_CHAR_LITERAL - } - - Select - - ParenthesedSelect - - ParenthesedExpressionList - -> - - Expression - . - - RelObjectName - COLLATE - - S_CHAR_LITERAL - - S_QUOTED_IDENTIFIER - - S_IDENTIFIER - - IntervalExpressionWithoutInterval - - ArrayExpression - :: - - ColDataType - -> - - : - - ->> - - #> - - #>> - - Expression - - SimpleExpression - - JsonExpression - AT - - K_DATETIMELITERAL - ZONE - - PrimaryExpression - -
- - -
         ::= ( 'NOT' | '!' )? ( '+' | '-' | '~' )? ( 'NULL' | CaseWhenExpression | CharacterPrimary | ImplicitCast | JdbcParameter | JdbcNamedParameter | UserVariable | NumericBind | ExtractExpression | XMLSerializeExpr | JsonFunction | JsonAggregateFunction | FullTextSearch | CastExpression | Function AnalyticExpression? | DateUnitExpression | IntervalExpression | S_DOUBLE | S_LONG | S_HEX | AllColumns | AllTableColumns | K_TIME_KEY_EXPR | 'CURRENT' | DateTimeLiteralExpression | StructType | ( 'ARRAY' ( '<' ColDataType '>' )? )? ArrayConstructor | NextValExpression | ConnectByRootOperator | ConnectByPriorOperator | 'ALL' | Column ( '(' '+' ')' )? | 'TRUE' | 'FALSE' | S_CHAR_LITERAL | ( '{d' | '{t' | '{ts' ) S_CHAR_LITERAL '}' | Select | ParenthesedSelect | ParenthesedExpressionList ( '->' Expression )? ( '.' RelObjectName )* ) ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER | S_IDENTIFIER ) )? IntervalExpressionWithoutInterval? ArrayExpression? ( '::' ColDataType )* ( ( ( '->' | ':' | '->>' | '#>' | '#>>' ) ( Expression | SimpleExpression ) )+ JsonExpression )? ( 'AT' K_DATETIMELITERAL 'ZONE' PrimaryExpression )*
-
- - -====================================================================================================================== -ConnectByRootOperator -====================================================================================================================== - - -.. raw:: html - - - - - - CONNECT_BY_ROOT - - Expression - -
- - -
         ::= 'CONNECT_BY_ROOT' Expression
-
- Referenced by: -
- - -====================================================================================================================== -ConnectByPriorOperator -====================================================================================================================== - - -.. raw:: html - - - - - - PRIOR - - Expression - -
- - -
         ::= 'PRIOR' Expression
-
- Referenced by: -
- - -====================================================================================================================== -NextValExpression -====================================================================================================================== - - -.. raw:: html - - - - - - K_NEXTVAL - - RelObjectNames - -
- - -
         ::= K_NEXTVAL RelObjectNames
-
- Referenced by: -
- - -====================================================================================================================== -JdbcNamedParameter -====================================================================================================================== - - -.. raw:: html - - - - - - : - - & - - IdentifierChain - -
- - -
         ::= ( ':' | '&' ) IdentifierChain
-
- - -====================================================================================================================== -OracleNamedFunctionParameter -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNameExt - OUTER - - => - - Expression - -
- - -
         ::= ( RelObjectNameExt | 'OUTER' ) '=>' Expression
-
- Referenced by: -
- - -====================================================================================================================== -PostgresNamedFunctionParameter -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNameExt - OUTER - - := - - Expression - -
- - -
         ::= ( RelObjectNameExt | 'OUTER' ) ':=' Expression
-
- Referenced by: -
- - -====================================================================================================================== -UserVariable -====================================================================================================================== - - -.. raw:: html - - - - - - S_AT_IDENTIFIER - - IdentifierChain2 - -
- - -
         ::= S_AT_IDENTIFIER IdentifierChain2
-
- - -====================================================================================================================== -NumericBind -====================================================================================================================== - - -.. raw:: html - - - - - - : - - S_LONG - -
- - -
         ::= ':' S_LONG
-
- Referenced by: -
- - -====================================================================================================================== -DateTimeLiteralExpression -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATETIMELITERAL - - S_CHAR_LITERAL - - S_QUOTED_IDENTIFIER - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -DateUnitExpression -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATE_LITERAL - -
- - -
         ::= K_DATE_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -RangeExpression -====================================================================================================================== - - -.. raw:: html - - - - - - : - - Expression - -
- - -
         ::= ':' Expression
-
- Referenced by: -
- - -====================================================================================================================== -ArrayConstructor -====================================================================================================================== - - -.. raw:: html - - - - - - [ - - Expression - - RangeExpression - - ArrayConstructor - , - - ] - - -
- - -
         ::= '[' ( ( Expression RangeExpression? | ArrayConstructor ) ( ',' ( Expression RangeExpression? | ArrayConstructor ) )* )? ']'
-
- - -====================================================================================================================== -StructParameters -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - - ColDataType - , - - -
- - -
         ::= RelObjectName? ColDataType ( ',' RelObjectName? ColDataType )*
-
- Referenced by: -
- - -====================================================================================================================== -StructType -====================================================================================================================== - - -.. raw:: html - - - - - - STRUCT - - < - - StructParameters - > - - ( - - SelectItemsList - ) - - { - - RelObjectNameExt - - S_CHAR_LITERAL - : - - Expression - , - - } - - :: - - STRUCT - - ( - - StructParameters - ) - - -
- - -
         ::= 'STRUCT' ( '<' StructParameters '>' )? '(' SelectItemsList ')'
-
           | '{' ( RelObjectNameExt | S_CHAR_LITERAL ) ':' Expression ( ',' ( RelObjectNameExt | S_CHAR_LITERAL ) ':' Expression )* '}' ( '::' 'STRUCT' '(' StructParameters ')' )*
-
- Referenced by: -
- - -====================================================================================================================== -JsonExpression -====================================================================================================================== - - -.. raw:: html - - - - - - :: - - ColDataType - -> - - : - - ->> - - #> - - #>> - - Expression - - SimpleExpression - -
- - -
         ::= ( ( '::' ColDataType )+ ( ( '->' | ':' | '->>' | '#>' | '#>>' ) ( Expression | SimpleExpression ) )* )*
-
- Referenced by: -
- - -====================================================================================================================== -JsonKeyValuePair -====================================================================================================================== - - -.. raw:: html - - - - - - KEY - - S_CHAR_LITERAL - - Column - - AllTableColumns - - AllColumns - - Expression - VALUE - - : - - , - - Expression - FORMAT - - JSON - - ENCODING - - JsonEncoding - -
- - -
         ::= ( 'KEY'? ( S_CHAR_LITERAL | Column ) | AllTableColumns | AllColumns | Expression ) ( ( 'VALUE' | ':' | ',' ) Expression )? ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )?
-
- Referenced by: -
- - -====================================================================================================================== -JsonObjectBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonKeyValuePair - , - - NULL - - ABSENT - - ON - - NULL - - STRICT - - WITH - - WITHOUT - - UNIQUE - - KEYS - - RETURNING - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - ) - - -
- - -
         ::= '(' ( JsonKeyValuePair ( ',' JsonKeyValuePair )* )? ( ( 'NULL' | 'ABSENT' ) 'ON' 'NULL' )? 'STRICT'? ( ( 'WITH' | 'WITHOUT' ) 'UNIQUE' - 'KEYS' )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonArrayBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - NULL - - ON - - NULL - - Expression - FORMAT - - JSON - - ENCODING - - JsonEncoding - , - - ABSENT - - ON - - NULL - - RETURNING - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - ) - - -
- - -
         ::= '(' ( 'NULL' 'ON' 'NULL' | Expression ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? ( ',' Expression ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )* )* ( 'ABSENT' 'ON' 'NULL' )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonKeyword -====================================================================================================================== - - -.. raw:: html - - - - - - S_IDENTIFIER - -
- - -
         ::= S_IDENTIFIER
-
- - -====================================================================================================================== -JsonEncoding -====================================================================================================================== - - -.. raw:: html - - - - - - S_IDENTIFIER - -
- - -
         ::= S_IDENTIFIER
-
- - -====================================================================================================================== -JsonValueOrQueryInputExpression -====================================================================================================================== - - -.. raw:: html - - - - - - Expression - FORMAT - - JSON - - ENCODING - - JsonEncoding - -
- - -
         ::= Expression ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )?
-
- - -====================================================================================================================== -JsonValueOnResponseBehavior -====================================================================================================================== - - -.. raw:: html - - - - - - ERROR - - NULL - - DEFAULT - - Expression - -
- - -
         ::= 'ERROR'
-
           | 'NULL'
-
           | 'DEFAULT' Expression
-
- - -====================================================================================================================== -JsonQueryOnResponseBehavior -====================================================================================================================== - - -.. raw:: html - - - - - - ERROR - - NULL - - S_IDENTIFIER - ARRAY - - JsonKeyword - -
- - -
         ::= 'ERROR'
-
           | 'NULL'
-
           | S_IDENTIFIER ( 'ARRAY' | JsonKeyword )
-
- Referenced by: -
- - -====================================================================================================================== -JsonExistsOnResponseBehavior -====================================================================================================================== - - -.. raw:: html - - - - - - TRUE - - FALSE - - UNKNOWN - - ERROR - - -
- - -
         ::= 'TRUE'
-
           | 'FALSE'
-
           | 'UNKNOWN'
-
           | 'ERROR'
-
- Referenced by: -
- - -====================================================================================================================== -JsonExistsBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonValueOrQueryInputExpression - , - - Expression - - JsonKeyword - - Expression - , - - JsonExistsOnResponseBehavior - ON - - ERROR - - ) - - -
- - -
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( JsonExistsOnResponseBehavior 'ON' 'ERROR' )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonValueBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonValueOrQueryInputExpression - , - - Expression - - JsonKeyword - - Expression - , - - RETURNING - - ColDataType - - JsonValueOnResponseBehavior - ON - - JsonKeyword - - JsonValueOnResponseBehavior - ON - - ERROR - - ) - - -
- - -
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType )? ( JsonValueOnResponseBehavior 'ON' JsonKeyword )? ( JsonValueOnResponseBehavior 'ON' 'ERROR' )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonQueryBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonValueOrQueryInputExpression - , - - Expression - - JsonKeyword - - Expression - , - - RETURNING - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - WITHOUT - - WITH - - S_IDENTIFIER - ARRAY - - JsonKeyword - KEEP - - JsonKeyword - - JsonKeyword - ON - - JsonKeyword - STRING - - JsonQueryOnResponseBehavior - ON - - JsonKeyword - - JsonQueryOnResponseBehavior - ON - - ERROR - - Expression - , - - ) - - -
- - -
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' JsonKeyword )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? ( ',' Expression ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' JsonKeyword )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonFunction -====================================================================================================================== - - -.. raw:: html - - - - - - JSON_OBJECT - - JsonObjectBody - JSON_ARRAY - - JsonArrayBody - - JsonKeyword - - JsonValueBody - - JsonQueryBody - - JsonExistsBody - -
- - -
         ::= 'JSON_OBJECT' JsonObjectBody
-
           | 'JSON_ARRAY' JsonArrayBody
-
           | JsonKeyword ( JsonValueBody | JsonQueryBody | JsonExistsBody )
-
- Referenced by: -
- - -====================================================================================================================== -JsonAggregateFunction -====================================================================================================================== - - -.. raw:: html - - - - - - JSON_OBJECTAGG - - ( - - KEY - - DT_ZONE - - S_DOUBLE - - S_LONG - - S_HEX - - S_CHAR_LITERAL - - Column - : - - , - - VALUE - - Expression - FORMAT - - JSON - - NULL - - ABSENT - - ON - - NULL - - WITH - - WITHOUT - - UNIQUE - - KEYS - - JSON_ARRAYAGG - - ( - - Expression - FORMAT - - JSON - - OrderByElements - NULL - - ABSENT - - ON - - NULL - - ) - - FILTER - - ( - - WHERE - - Expression - ) - - OVER - - ( - - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - OrderByElements - - WindowElement - ) - - -
- - -
         ::= ( 'JSON_OBJECTAGG' '(' 'KEY'? ( DT_ZONE | S_DOUBLE | S_LONG | S_HEX | S_CHAR_LITERAL | Column ) ( ':' | ',' | 'VALUE' ) Expression ( 'FORMAT' 'JSON' )? ( ( 'NULL' | 'ABSENT' ) 'ON' 'NULL' )? ( ( 'WITH' | 'WITHOUT' - ) 'UNIQUE' 'KEYS' )? | 'JSON_ARRAYAGG' '(' Expression ( 'FORMAT' 'JSON' )? OrderByElements? ( ( 'NULL' | 'ABSENT' ) 'ON' 'NULL' )? ) ')' ( 'FILTER' '(' 'WHERE' Expression ')' )? ( 'OVER' '(' ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? OrderByElements? WindowElement? ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -IntervalExpression -====================================================================================================================== - - -.. raw:: html - - - - - - INTERVAL - - - - - S_LONG - - S_DOUBLE - - S_CHAR_LITERAL - - Expression - - S_IDENTIFIER - - K_DATE_LITERAL - -
- - -
         ::= 'INTERVAL' ( '-'? ( S_LONG | S_DOUBLE ) | S_CHAR_LITERAL | Expression ) ( S_IDENTIFIER | K_DATE_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -IntervalExpressionWithoutInterval -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATE_LITERAL - -
- - -
         ::= K_DATE_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -KeepExpression -====================================================================================================================== - - -.. raw:: html - - - - - - KEEP - - ( - - S_IDENTIFIER - FIRST - - LAST - - OrderByElements - ) - - -
- - -
         ::= 'KEEP' '(' S_IDENTIFIER ( 'FIRST' | 'LAST' ) OrderByElements ')'
-
- Referenced by: -
- - -====================================================================================================================== -windowFun -====================================================================================================================== - - -.. raw:: html - - - - - - OVER - - WITHIN - - GROUP - - RelObjectName - - windowDefinition - OVER - - ( - - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - ) - - -
- - -
         ::= ( 'OVER' | 'WITHIN' 'GROUP' ) ( RelObjectName | windowDefinition ( 'OVER' '(' ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? ')' )? )
-
- Referenced by: -
- - -====================================================================================================================== -windowDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - OrderByElements - - WindowElement - ) - - -
- - -
         ::= '(' ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? OrderByElements? WindowElement? ')'
-
- Referenced by: -
- - -====================================================================================================================== -AnalyticExpression -====================================================================================================================== - - -.. raw:: html - - - - - - FILTER - - ( - - WHERE - - Expression - ) - - windowFun - - windowFun - -
- - -
         ::= 'FILTER' '(' 'WHERE' Expression ')' windowFun?
-
           | windowFun
-
- Referenced by: -
- - -====================================================================================================================== -WindowElement -====================================================================================================================== - - -.. raw:: html - - - - - - ROWS - - RANGE - - BETWEEN - - WindowOffset - AND - - WindowOffset - -
- - -
         ::= ( 'ROWS' | 'RANGE' ) ( 'BETWEEN' WindowOffset 'AND' )? WindowOffset
-
- - -====================================================================================================================== -WindowOffset -====================================================================================================================== - - -.. raw:: html - - - - - - UNBOUNDED - - SimpleExpression - PRECEDING - - FOLLOWING - - CURRENT - - ROW - - -
- - -
         ::= ( 'UNBOUNDED' | SimpleExpression ) ( 'PRECEDING' | 'FOLLOWING' )
-
           | 'CURRENT' 'ROW'
-
- Referenced by: -
- - -====================================================================================================================== -ExtractExpression -====================================================================================================================== - - -.. raw:: html - - - - - - EXTRACT - - ( - - RelObjectName - - S_CHAR_LITERAL - FROM - - SimpleExpression - ) - - -
- - -
         ::= 'EXTRACT' '(' ( RelObjectName | S_CHAR_LITERAL ) 'FROM' SimpleExpression ')'
-
- Referenced by: -
- - -====================================================================================================================== -ImplicitCast -====================================================================================================================== - - -.. raw:: html - - - - - - DataType - - S_CHAR_LITERAL - - S_LONG - - S_DOUBLE - -
- - -
         ::= DataType ( S_CHAR_LITERAL | S_LONG | S_DOUBLE )
-
- Referenced by: -
- - -====================================================================================================================== -CastExpression -====================================================================================================================== - - -.. raw:: html - - - - - - CAST - - SAFE_CAST - - TRY_CAST - - INTERPRET - - ( - - SimpleExpression - AS - - ROW - - ( - - ColumnDefinition - , - - ) - - ColDataType - FORMAT - - S_CHAR_LITERAL - ) - - -
- - -
         ::= ( 'CAST' | 'SAFE_CAST' | 'TRY_CAST' | 'INTERPRET' ) '(' SimpleExpression 'AS' ( 'ROW' '(' ColumnDefinition ( ',' ColumnDefinition )* ')' | ColDataType ) ( 'FORMAT' S_CHAR_LITERAL )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -CaseWhenExpression -====================================================================================================================== - - -.. raw:: html - - - - - - CASE - - Expression - - WhenThenSearchCondition - ELSE - - Expression - - SimpleExpression - END - - -
- - -
         ::= 'CASE' Expression? WhenThenSearchCondition+ ( 'ELSE' ( Expression | SimpleExpression ) )? 'END'
-
- Referenced by: -
- - -====================================================================================================================== -WhenThenSearchCondition -====================================================================================================================== - - -.. raw:: html - - - - - - WHEN - - Expression - THEN - - Expression - - SimpleExpression - -
- - -
         ::= 'WHEN' Expression 'THEN' ( Expression | SimpleExpression )
-
- Referenced by: -
- - -====================================================================================================================== -RowConstructor -====================================================================================================================== - - -.. raw:: html - - - - - - ROW - - ParenthesedExpressionList - -
- - -
         ::= 'ROW' ParenthesedExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -VariableExpression -====================================================================================================================== - - -.. raw:: html - - - - - - UserVariable - = - - SimpleExpression - -
- - -
         ::= UserVariable '=' SimpleExpression
-
- Not referenced by any. -
- - -====================================================================================================================== -Execute -====================================================================================================================== - - -.. raw:: html - - - - - - EXEC - - EXECUTE - - CALL - - RelObjectNames - - ExpressionList - -
- -
Execute  ::= ( 'EXEC' | 'EXECUTE' | 'CALL' ) RelObjectNames ExpressionList?
-
- Referenced by: -
- - -====================================================================================================================== -FullTextSearch -====================================================================================================================== - - -.. raw:: html - - - - - - MATCH - - ( - - ColumnList - ) - - AGAINST - - ( - - S_CHAR_LITERAL - - JdbcParameter - - JdbcNamedParameter - IN NATURAL LANGUAGE MODE - - IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION - - IN BOOLEAN MODE - - WITH QUERY EXPANSION - - ) - - -
- - -
         ::= 'MATCH' '(' ColumnList ')' 'AGAINST' '(' ( S_CHAR_LITERAL | JdbcParameter | JdbcNamedParameter ) ( 'IN NATURAL LANGUAGE MODE' | 'IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION' - | 'IN BOOLEAN MODE' | 'WITH QUERY EXPANSION' )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -LambdaExpression -====================================================================================================================== - - -.. raw:: html - - - - - - ParenthesedColumnList - - RelObjectName - -> - - Expression - -
- - -
         ::= ( ParenthesedColumnList | RelObjectName ) '->' Expression
-
- - -====================================================================================================================== -Function -====================================================================================================================== - - -.. raw:: html - - - - - - { - - FN - - InternalFunction - } - - SpecialStringFunctionWithNamedParameters - - InternalFunction - -
- -
Function ::= '{' 'FN' InternalFunction '}'
- -
           | InternalFunction
-
- - -====================================================================================================================== -SpecialStringFunctionWithNamedParameters -====================================================================================================================== - - -.. raw:: html - - - - - - K_STRING_FUNCTION_NAME - ( - - NamedExpressionListExprFirst - - ExpressionList - ) - - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -InternalFunction -====================================================================================================================== - - -.. raw:: html - - - - - - APPROXIMATE - - RelObjectNames - ( - - DISTINCT - - ALL - - UNIQUE - - TABLE - - ExpressionList - - OrderByElements - ON - - OVERFLOW - - TRUNCATE - - ERROR - - S_CHAR_LITERAL - WITH - - WITHOUT - - COUNT - - Select - HAVING - - MIN - - MAX - - Expression - IGNORE - - RESPECT - - NULLS - - PlainLimit - ) - - ( - - ExpressionList - ) - - . - - Function - - Column - IGNORE - - RESPECT - - NULLS - - KeepExpression - -
- - -
         ::= 'APPROXIMATE'? RelObjectNames '(' ( ( 'DISTINCT' | 'ALL' | 'UNIQUE' )? ( 'TABLE'? ExpressionList OrderByElements? ( 'ON' 'OVERFLOW' ( 'TRUNCATE' | 'ERROR' ) ( S_CHAR_LITERAL ( ( 'WITH' | 'WITHOUT' ) 'COUNT' )? )? )? | Select ) )? ( 'HAVING' ( 'MIN' | 'MAX' ) Expression )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? PlainLimit? ')' ( '(' ExpressionList ')' )? ( '.' ( Function | Column ) )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? KeepExpression?
-
- Referenced by: -
- - -====================================================================================================================== -XMLSerializeExpr -====================================================================================================================== - - -.. raw:: html - - - - - - XMLSERIALIZE - - ( - - XMLAGG - - ( - - XMLTEXT - - ( - - SimpleExpression - ) - - OrderByElements - ) - - AS - - ColDataType - ) - - -
- - -
         ::= 'XMLSERIALIZE' '(' 'XMLAGG' '(' 'XMLTEXT' '(' SimpleExpression ')' OrderByElements? ')' 'AS' ColDataType ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonTablePassingClause -====================================================================================================================== - - -.. raw:: html - - - - - - Expression - AS - - RelObjectName - -
- - -
         ::= Expression 'AS' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableOnEmptyBehavior -====================================================================================================================== - - -.. raw:: html - - - - - - ERROR - - NULL - - DEFAULT - - Expression - - S_IDENTIFIER - - JsonKeyword - ARRAY - - -
- - -
         ::= 'ERROR'
-
           | 'NULL'
-
           | 'DEFAULT' Expression
-
           | S_IDENTIFIER ( JsonKeyword | 'ARRAY' )?
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableWrapperClause -====================================================================================================================== - - -.. raw:: html - - - - - - WITHOUT - - WITH - - S_IDENTIFIER - ARRAY - - JsonKeyword - -
- - -
         ::= ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableQuotesClause -====================================================================================================================== - - -.. raw:: html - - - - - - KEEP - - JsonKeyword - - JsonKeyword - ON - - JsonKeyword - STRING - - -
- - -
         ::= ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )?
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableColumnDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - JsonKeyword - PATH - - Expression - AS - - RelObjectName - - JsonTableColumnsClause - - RelObjectName - FOR - - JsonKeyword - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - PATH - - Expression - - JsonTableWrapperClause - - JsonTableQuotesClause - - JsonTableOnEmptyBehavior - ON - - JsonKeyword - - JsonValueOnResponseBehavior - ON - - ERROR - - -
- - -
         ::= JsonKeyword 'PATH'? Expression ( 'AS' RelObjectName )? JsonTableColumnsClause
-
           | RelObjectName ( 'FOR' JsonKeyword | ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? ( 'PATH' Expression )? JsonTableWrapperClause? JsonTableQuotesClause? ( JsonTableOnEmptyBehavior 'ON' JsonKeyword )? ( JsonValueOnResponseBehavior 'ON' 'ERROR' )? )
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableColumnsClause -====================================================================================================================== - - -.. raw:: html - - - - - - COLUMNS - - ( - - JsonTableColumnDefinition - , - - ) - - -
- - -
         ::= 'COLUMNS' '(' ( JsonTableColumnDefinition ( ',' JsonTableColumnDefinition )* )? ')'
-
- - -====================================================================================================================== -JsonTablePlanTerm -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonTablePlanExpression - ) - - RelObjectName - - Expression - -
- - -
         ::= '(' JsonTablePlanExpression ')'
-
           | RelObjectName
-
           | Expression
-
- Referenced by: -
- - -====================================================================================================================== -JsonTablePlanExpression -====================================================================================================================== - - -.. raw:: html - - - - - - JsonTablePlanTerm - , - - INNER - - OUTER - - CROSS - - UNION - - -
- - -
         ::= JsonTablePlanTerm ( ( ',' | 'INNER' | 'OUTER' | 'CROSS' | 'UNION' ) JsonTablePlanTerm )*
-
- - -====================================================================================================================== -JsonTablePlanClause -====================================================================================================================== - - -.. raw:: html - - - - - - PLAN - - DEFAULT - - ( - - JsonTablePlanExpression - ) - - -
- - -
         ::= 'PLAN' 'DEFAULT'? '(' JsonTablePlanExpression ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableOnErrorClause -====================================================================================================================== - - -.. raw:: html - - - - - - ERROR - - S_IDENTIFIER - ON - - ERROR - - -
- - -
         ::= ( 'ERROR' | S_IDENTIFIER ) 'ON' 'ERROR'
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Expression - , - - Expression - AS - - RelObjectName - - JsonKeyword - - JsonTablePassingClause - , - - JsonTableColumnsClause - - JsonTablePlanClause - - JsonTableOnErrorClause - ) - - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -TableFunction -====================================================================================================================== - - -.. raw:: html - - - - - - LATERAL - - JsonKeyword - - JsonTableBody - - Function - WITH - - OFFSET - - ORDINALITY - - -
- - -
         ::= 'LATERAL'? ( JsonKeyword JsonTableBody | Function ) ( 'WITH' ( 'OFFSET' | 'ORDINALITY' ) )?
-
- Referenced by: -
- - -====================================================================================================================== -ColumnNamesWithParamsList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - RelObjectName - - CreateParameter - , - - ) - - -
- - -
         ::= '(' RelObjectName CreateParameter? ( ',' RelObjectName CreateParameter? )* ')'
-
- - -====================================================================================================================== -IndexColumnWithParams -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ( - - Expression - ) - - CreateParameter - -
- - -
         ::= ( RelObjectName | '(' Expression ')' ) CreateParameter?
-
- Referenced by: -
- - -====================================================================================================================== -IndexColumnsWithParamsList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - IndexColumnWithParams - , - - ) - - -
- - -
         ::= '(' IndexColumnWithParams ( ',' IndexColumnWithParams )* ')'
-
- - -====================================================================================================================== -Index -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNames - -
- - -
- Referenced by: -
- - -====================================================================================================================== -CreateIndex -====================================================================================================================== - - -.. raw:: html - - - - - - CreateParameter - INDEX - - IF - - NOT - - EXISTS - - Index - ON - - Table - - UsingIndexType - - UsingIndexType - ON - - Table - - IndexColumnsWithParamsList - - CreateParameter - -
- - -
         ::= CreateParameter? 'INDEX' ( 'IF' 'NOT' 'EXISTS' )? Index ( 'ON' Table UsingIndexType? | UsingIndexType? 'ON' Table ) IndexColumnsWithParamsList CreateParameter*
-
- Referenced by: -
- - -====================================================================================================================== -ColumnDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - - ColDataType - - CreateParameter - -
- - - -
- - -====================================================================================================================== -CreateSchema -====================================================================================================================== - - -.. raw:: html - - - - - - SCHEMA - - IF - - NOT - - EXISTS - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - . - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - AUTHORIZATION - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - PathSpecification - CREATE - - CreateTable - - CreateView - -
- - -
         ::= 'SCHEMA' ( 'IF' 'NOT' 'EXISTS' )? ( ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ( '.' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) )? )? ( 'AUTHORIZATION' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) )? PathSpecification? ( 'CREATE' CreateTable | CreateView )*
-
- Referenced by: -
- - -====================================================================================================================== -PathSpecification -====================================================================================================================== - - -.. raw:: html - - - - - - PATH - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - , - - -
- - -
         ::= 'PATH' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ( ',' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) )*
-
- Referenced by: -
- - -====================================================================================================================== -CreateTableConstraint -====================================================================================================================== - - -.. raw:: html - - - - - - INDEX - - UNIQUE - - FULLTEXT - - SPATIAL - - KEY - - RelObjectName - - IndexColumnsWithParamsList - - CreateParameter - CONSTRAINT - - RelObjectName - PRIMARY - - KEY - - UNIQUE - - KEY - - ColumnNamesWithParamsList - - CreateParameter - - ForeignKeySpec - - CheckConstraintSpec - EXCLUDE - - WHERE - - ( - - Expression - ) - - -
- - -
         ::= ( 'INDEX' | 'UNIQUE'? ( 'FULLTEXT' | 'SPATIAL' )? 'KEY' ) RelObjectName IndexColumnsWithParamsList CreateParameter*
-
           | ( 'CONSTRAINT' RelObjectName )? ( ( 'PRIMARY' 'KEY' | 'UNIQUE' 'KEY'? ) ColumnNamesWithParamsList CreateParameter* | ForeignKeySpec | CheckConstraintSpec )
-
           | 'EXCLUDE' 'WHERE' ( '(' Expression ')' )*
-
- Referenced by: -
- - -====================================================================================================================== -CreateTable -====================================================================================================================== - - -.. raw:: html - - - - - - UNLOGGED - - GLOBAL - - CreateParameter - TABLE - - IF - - NOT - - EXISTS - - Table - ( - - RelObjectName - , - - ColumnDefinition - , - - CreateTableConstraint - - ColumnDefinition - ) - - CreateParameter - - RowMovement - AS - - Select - LIKE - - ( - - Table - ) - - Table - , - - SpannerInterleaveIn - -
- - -
         ::= 'UNLOGGED'? 'GLOBAL'? CreateParameter* 'TABLE' ( 'IF' 'NOT' 'EXISTS' )? Table ( '(' ( RelObjectName ( ',' RelObjectName )* | ColumnDefinition ( ',' ( CreateTableConstraint | ColumnDefinition ) )* ) ')' )? CreateParameter* RowMovement? ( 'AS' Select )? ( 'LIKE' ( '(' Table ')' | Table ) )? ( ',' SpannerInterleaveIn )?
-
- Referenced by: -
- - -====================================================================================================================== -SpannerInterleaveIn -====================================================================================================================== - - -.. raw:: html - - - - - - INTERLEAVE - - IN - - PARENT - - Table - ON - - DELETE - - NO - - ACTION - - CASCADE - - -
- - -
         ::= 'INTERLEAVE' 'IN' 'PARENT' Table ( 'ON' 'DELETE' ( 'NO' 'ACTION' | 'CASCADE' ) )?
-
- Referenced by: -
- - -====================================================================================================================== -DataType -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATETIMELITERAL - - DT_ZONE - - DATA_TYPE - SIGNED - - UNSIGNED - - CHARACTER - - BIT - - BYTES - - BINARY - - BOOLEAN - - CHAR - - JSON - - STRING - - DATA_TYPE - SIGNED - - UNSIGNED - - CHARACTER - - BIT - - BYTES - - BINARY - - BOOLEAN - - CHAR - - JSON - - STRING - - ( - - S_LONG - MAX - - , - - S_LONG - ) - - K_TEXT_LITERAL - ARRAY - - < - - ColDataType - > - - -
- - -
           | 'ARRAY' '<' ColDataType '>'
-
           | ( K_DATETIMELITERAL | DT_ZONE | DATA_TYPE | 'SIGNED' | 'UNSIGNED' | 'CHARACTER' | 'BIT' | 'BYTES' | 'BINARY' | 'BOOLEAN' | - 'CHAR' | 'JSON' | 'STRING' ) ( DATA_TYPE | 'SIGNED' | 'UNSIGNED' | 'CHARACTER' | 'BIT' | 'BYTES' | 'BINARY' | 'BOOLEAN' | - 'CHAR' | 'JSON' | 'STRING' )* ( '(' ( S_LONG | 'MAX' ) ( ',' S_LONG )? ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -ColDataType -====================================================================================================================== - - -.. raw:: html - - - - - - STRUCT - - ( - - RelObjectNameExt - - ColDataType - , - - ) - - DataType - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - K_DATETIMELITERAL - - K_DATE_LITERAL - XML - - INTERVAL - - DT_ZONE - CHAR - - SET - - BINARY - - JSON - - STRING - - PUBLIC - - DATA - - NAME - - . - - ColDataType - ( - - S_LONG - MAX - - BYTE - - CHAR - - S_CHAR_LITERAL - - S_IDENTIFIER - CHAR - - , - - ) - - [ - - S_LONG - ] - - CHARACTER - - SET - - S_IDENTIFIER - BINARY - - -
- - -
         ::= ( 'STRUCT' '(' RelObjectNameExt ColDataType ( ',' RelObjectNameExt ColDataType )* ')' | DataType | ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | K_DATETIMELITERAL | K_DATE_LITERAL | 'XML' | 'INTERVAL' | DT_ZONE | 'CHAR' | 'SET' | 'BINARY' | 'JSON' | 'STRING' | 'PUBLIC' | 'DATA' | 'NAME' ) ( - '.' ColDataType )? ) ( '(' ( ( ( S_LONG | 'MAX' ) ( 'BYTE' | 'CHAR' )? | S_CHAR_LITERAL | S_IDENTIFIER | 'CHAR' ) ','? )* ')' )? ( '[' S_LONG? ']' )* ( 'CHARACTER' 'SET' ( S_IDENTIFIER | 'BINARY' ) )?
-
- - -====================================================================================================================== -Analyze -====================================================================================================================== - - -.. raw:: html - - - - - - ANALYZE - - Table - -
- -
Analyze  ::= 'ANALYZE' Table
-
- Referenced by: -
- - -====================================================================================================================== -ColumnWithCommentList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Column - , - - ) - - -
- - -
         ::= '(' Column ( ',' Column )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -CreateView -====================================================================================================================== - - -.. raw:: html - - - - - - NO - - FORCE - - SECURE - - TEMP - - TEMPORARY - - VOLATILE - - MATERIALIZED - - VIEW - - Table - AUTO - - REFRESH - - YES - - NO - - IF - - NOT - - EXISTS - - ColumnWithCommentList - - CreateViewTailComment - AS - - Select - WITH - - READ - - ONLY - - -
- - -
         ::= ( 'NO'? 'FORCE' )? 'SECURE'? ( 'TEMP' | 'TEMPORARY' | 'VOLATILE' )? 'MATERIALIZED'? - 'VIEW' Table ( 'AUTO' 'REFRESH' ( 'YES' | 'NO' ) )? ( 'IF' 'NOT' 'EXISTS' )? ColumnWithCommentList? CreateViewTailComment? 'AS' Select ( 'WITH' 'READ' 'ONLY' )?
-
- Referenced by: -
- - -====================================================================================================================== -CreateViewTailComment -====================================================================================================================== - - -.. raw:: html - - - - - - COMMENT - - = - - S_CHAR_LITERAL - -
- - -
         ::= 'COMMENT' '='? S_CHAR_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -Action -====================================================================================================================== - - -.. raw:: html - - - - - - CASCADE - - RESTRICT - - NO - - ACTION - - SET - - NULL - - DEFAULT - - -
- -
Action   ::= 'CASCADE'
-
           | 'RESTRICT'
-
           | 'NO' 'ACTION'
-
           | 'SET' ( 'NULL' | 'DEFAULT' )
-
- Referenced by: -
- - -====================================================================================================================== -ReferentialActionsOnIndex -====================================================================================================================== - - -.. raw:: html - - - - - - ON - - DELETE - - UPDATE - - Action - ON - - DELETE - - UPDATE - - Action - -
- - -
         ::= ( 'ON' ( 'DELETE' | 'UPDATE' ) Action )? ( 'ON' ( 'DELETE' | 'UPDATE' ) Action )?
-
- - -====================================================================================================================== -CheckConstraintSpec -====================================================================================================================== - - -.. raw:: html - - - - - - CHECK - - ( - - Expression - ) - - -
- - -
         ::= 'CHECK' ( '(' Expression ')' )*
-
- - -====================================================================================================================== -ForeignKeySpec -====================================================================================================================== - - -.. raw:: html - - - - - - FOREIGN - - KEY - - ColumnNamesWithParamsList - REFERENCES - - Table - - ColumnsNamesList - - ReferentialActionsOnIndex - -
- - -
         ::= 'FOREIGN' 'KEY' ColumnNamesWithParamsList 'REFERENCES' Table ColumnsNamesList? ReferentialActionsOnIndex
-
- - -====================================================================================================================== -AlterExpressionUsingIndex -====================================================================================================================== - - -.. raw:: html - - - - - - USING - - INDEX - - RelObjectName - -
- - -
         ::= 'USING' 'INDEX'? RelObjectName
-
- - -====================================================================================================================== -AlterExpressionConstraintTail -====================================================================================================================== - - -.. raw:: html - - - - - - AlterExpressionConstraintState - - AlterExpressionUsingIndex - - IndexWithComment - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -AlterView -====================================================================================================================== - - -.. raw:: html - - - - - - VIEW - - Table - - ColumnsNamesList - AS - - Select - -
- - -
         ::= 'VIEW' Table ColumnsNamesList? 'AS' Select
-
- Referenced by: -
- - -====================================================================================================================== -CreateParameter -====================================================================================================================== - - -.. raw:: html - - - - - - K_NEXTVAL - ( - - S_CHAR_LITERAL - :: - - ColDataType - ) - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - NAME - - . - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - NAME - - USING - - INDEX - - TABLESPACE - - RelObjectName - - S_CHAR_LITERAL - NULL - - NOT - - AUTO_INCREMENT - - PRIMARY - - FOREIGN - - REFERENCES - - KEY - - STORED - - ON - - COMMIT - - DROP - - ROWS - - UNIQUE - - CASCADE - - DELETE - - UPDATE - - CONSTRAINT - - WITH - - EXCLUDE - - WHERE - - TEMP - - TEMPORARY - - PARTITION - - BY - - IN - - TYPE - - COMMENT - - USING - - COLLATE - - ASC - - DESC - - TRUE - - FALSE - - PARALLEL - - BINARY - - START - - ORDER - - K_TIME_KEY_EXPR - RAW - - HASH - - FIRST - - LAST - - SIGNED - - UNSIGNED - - ENGINE - - IDENTITY - - MATERIALIZED - - SAMPLE - - ALWAYS - - = - - DEFAULT - - AS - - CHECK - - ( - - Expression - ) - - + - - - - - S_LONG - - S_DOUBLE - - AList - CHARACTER - - SET - - ARRAY - - ArrayConstructor - :: - - ColDataType - -
- - -
         ::= K_NEXTVAL '(' S_CHAR_LITERAL '::' ColDataType ')'
-
           | ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | 'NAME' ) ( '.' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | 'NAME' ) )?
-
           | ( 'USING' 'INDEX' )? 'TABLESPACE' RelObjectName
-
           | S_CHAR_LITERAL
-
           | 'NULL'
-
           | 'NOT'
-
           | 'AUTO_INCREMENT'
-
           | 'PRIMARY'
-
           | 'FOREIGN'
-
           | 'REFERENCES'
-
           | 'KEY'
-
           | 'STORED'
-
           | 'ON'
-
           | 'COMMIT'
-
           | 'DROP'
-
           | 'ROWS'
-
           | 'UNIQUE'
-
           | 'CASCADE'
-
           | 'DELETE'
-
           | 'UPDATE'
-
           | 'CONSTRAINT'
-
           | 'WITH'
-
           | 'EXCLUDE'
-
           | 'WHERE'
-
           | 'TEMP'
-
           | 'TEMPORARY'
-
           | 'PARTITION'
-
           | 'BY'
-
           | 'IN'
-
           | 'TYPE'
-
           | 'COMMENT'
-
           | 'USING'
-
           | 'COLLATE'
-
           | 'ASC'
-
           | 'DESC'
-
           | 'TRUE'
-
           | 'FALSE'
-
           | 'PARALLEL'
-
           | 'BINARY'
-
           | 'START'
-
           | 'ORDER'
-
           | K_TIME_KEY_EXPR
-
           | 'RAW'
-
           | 'HASH'
-
           | 'FIRST'
-
           | 'LAST'
-
           | 'SIGNED'
-
           | 'UNSIGNED'
-
           | 'ENGINE'
-
           | 'IDENTITY'
-
           | 'MATERIALIZED'
-
           | 'SAMPLE'
-
           | 'ALWAYS'
-
           | '='
-
           | ( 'DEFAULT' | 'AS' | 'CHECK' ) ( '(' Expression ')' )?
-
           | ( '+' | '-' )? S_LONG
-
           | S_DOUBLE
-
           | AList
-
           | 'CHARACTER' 'SET'
-
           | 'ARRAY' ArrayConstructor
-
           | '::' ColDataType
-
- - -====================================================================================================================== -RowMovement -====================================================================================================================== - - -.. raw:: html - - - - - - ENABLE - - DISABLE - - ROW - - MOVEMENT - - -
- - -
         ::= ( 'ENABLE' | 'DISABLE' ) 'ROW' 'MOVEMENT'
-
- Referenced by: -
- - -====================================================================================================================== -AList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - S_LONG - - S_DOUBLE - - S_CHAR_LITERAL - TRUE - - FALSE - - RelObjectName - , - - = - - ) - - -
- -
AList    ::= '(' ( ( S_LONG | S_DOUBLE | S_CHAR_LITERAL | 'TRUE' | 'FALSE' | RelObjectName ) ( ',' | '=' )? )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -ColumnsNamesListItem -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ( - - S_LONG - ) - - ASC - - DESC - - -
- - -
         ::= RelObjectName ( '(' S_LONG ')' )? ( 'ASC' | 'DESC' )?
-
- Referenced by: -
- - -====================================================================================================================== -ColumnsNamesList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - ColumnsNamesListItem - , - - ) - - -
- - -
         ::= '(' ColumnsNamesListItem ( ',' ColumnsNamesListItem )* ')'
-
- - -====================================================================================================================== -FuncArgsListItem -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - - RelObjectName - ( - - S_LONG - ) - - -
- - -
         ::= RelObjectName RelObjectName? ( '(' S_LONG ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -FuncArgsList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - FuncArgsListItem - , - - ) - - -
- - -
         ::= '(' ( FuncArgsListItem ( ',' FuncArgsListItem )* )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -Drop -====================================================================================================================== - - -.. raw:: html - - - - - - DROP - - MATERIALIZED - - S_IDENTIFIER - TEMPORARY - - TABLE - - INDEX - - VIEW - - SCHEMA - - SEQUENCE - - FUNCTION - - IF - - EXISTS - - Table - - FuncArgsList - - S_IDENTIFIER - CASCADE - - RESTRICT - - ON - - Table - -
- -
Drop     ::= 'DROP' 'MATERIALIZED'? ( S_IDENTIFIER | 'TEMPORARY'? 'TABLE' | 'INDEX' | 'VIEW' | 'SCHEMA' | 'SEQUENCE' | 'FUNCTION' ) - ( 'IF' 'EXISTS' )? Table FuncArgsList? ( S_IDENTIFIER | 'CASCADE' | 'RESTRICT' | 'ON' Table )*
-
- Referenced by: -
- - -====================================================================================================================== -Truncate -====================================================================================================================== - - -.. raw:: html - - - - - - TRUNCATE - - TABLE - - ONLY - - Table - , - - CASCADE - - -
- -
Truncate ::= 'TRUNCATE' 'TABLE'? 'ONLY'? Table ( ',' Table )* 'CASCADE'?
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnChanges -====================================================================================================================== - - -.. raw:: html - - - - - - AlterExpressionColumnDropDefault - - AlterExpressionColumnSetDefault - - AlterExpressionColumnSetVisibility - ( - - AlterExpressionColumnDataType - , - - ) - - -
- - -
         ::= AlterExpressionColumnDropDefault
-
           | AlterExpressionColumnSetDefault
-
           | AlterExpressionColumnSetVisibility
-
           | '(' AlterExpressionColumnDataType ( ',' AlterExpressionColumnDataType )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnDataType -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - TYPE - - ColDataType - - CreateParameter - -
- - -
         ::= RelObjectName 'TYPE'? ColDataType? CreateParameter*
-
- - -====================================================================================================================== -AlterExpressionColumnDropNotNull -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - DROP - - NOT - - NULL - - -
- - -
         ::= RelObjectName 'DROP' 'NOT'? 'NULL'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnDropDefault -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - DROP - - DEFAULT - - -
- - -
         ::= RelObjectName 'DROP' 'DEFAULT'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnSetDefault -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - SET - - DEFAULT - - Expression - -
- - -
         ::= RelObjectName 'SET' 'DEFAULT' Expression
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnSetVisibility -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - SET - - VISIBLE - - INVISIBLE - - -
- - -
         ::= RelObjectName 'SET' ( 'VISIBLE' | 'INVISIBLE' )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionConstraintState -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - DEFERRABLE - - VALIDATE - - NOVALIDATE - - ENABLE - - DISABLE - - -
- - -
         ::= ( 'NOT'? 'DEFERRABLE' | 'VALIDATE' | 'NOVALIDATE' | 'ENABLE' | 'DISABLE' - )*
-
- - -====================================================================================================================== -IndexWithComment -====================================================================================================================== - - -.. raw:: html - - - - - - COMMENT - - S_CHAR_LITERAL - -
- - -
         ::= 'COMMENT' S_CHAR_LITERAL
-
- - -====================================================================================================================== -IndexOptionList -====================================================================================================================== - - -.. raw:: html - - - - - - IndexOption - -
- - -
         ::= IndexOption*
-
- Referenced by: -
- - -====================================================================================================================== -UsingIndexType -====================================================================================================================== - - -.. raw:: html - - - - - - USING - - RelObjectName - -
- - -
         ::= 'USING' RelObjectName
-
- - -====================================================================================================================== -IndexOption -====================================================================================================================== - - -.. raw:: html - - - - - - KEY_BLOCK_SIZE - - = - - S_LONG - WITH - - PARSER - - S_IDENTIFIER - COMMENT - - S_CHAR_LITERAL - VISIBLE - - INVISIBLE - - UsingIndexType - -
- - -
         ::= 'KEY_BLOCK_SIZE' '='? S_LONG
-
           | 'WITH' 'PARSER' S_IDENTIFIER
-
           | 'COMMENT' S_CHAR_LITERAL
-
           | 'VISIBLE'
-
           | 'INVISIBLE'
-
           | UsingIndexType
-
- Referenced by: -
- - -====================================================================================================================== -PartitionDefinitions -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - PARTITION - - RelObjectName - VALUES - - LESS - - THAN - - ( - - Expression - ) - - MAXVALUE - - ENGINE - - = - - S_IDENTIFIER - , - - ) - - -
- - -
         ::= '(' ( 'PARTITION' RelObjectName 'VALUES' 'LESS' 'THAN' ( '(' Expression ')' | 'MAXVALUE' ) ( 'ENGINE' '=' S_IDENTIFIER )? ','? )* ')'
-
- - -====================================================================================================================== -PartitionNamesList -====================================================================================================================== - - -.. raw:: html - - - - - - ALL - - S_IDENTIFIER - , - - -
- - -
         ::= 'ALL'
-
           | S_IDENTIFIER ( ',' S_IDENTIFIER )*
-
- - -====================================================================================================================== -AlterExpressionDiscardOrImport -====================================================================================================================== - - -.. raw:: html - - - - - - DISCARD - - IMPORT - - PARTITION - - PartitionNamesList - TABLESPACE - - -
- - -
         ::= ( 'DISCARD' | 'IMPORT' ) ( 'PARTITION' PartitionNamesList )? 'TABLESPACE'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionAddConstraint -====================================================================================================================== - - -.. raw:: html - - - - - - CONSTRAINT - - UNIQUE - - KEY - - INDEX - - RelObjectName - - ColumnsNamesList - - RelObjectName - FOREIGN - - KEY - - ColumnsNamesList - REFERENCES - - Table - - ColumnsNamesList - - ReferentialActionsOnIndex - KEY - - ColumnsNamesList - - AlterExpressionConstraintState - PRIMARY - - KEY - - UNIQUE - - KEY - - INDEX - - ColumnsNamesList - - AlterExpressionConstraintTail - NOT - - ENFORCED - - CheckConstraintSpec - -
- - -
         ::= 'CONSTRAINT' ( 'UNIQUE' ( 'KEY' | 'INDEX' )? RelObjectName ColumnsNamesList | RelObjectName ( ( 'FOREIGN' 'KEY' ColumnsNamesList 'REFERENCES' Table ColumnsNamesList? ReferentialActionsOnIndex | 'KEY' ColumnsNamesList ) AlterExpressionConstraintState | ( 'PRIMARY' 'KEY' | 'UNIQUE' ( 'KEY' | 'INDEX' )? ) ColumnsNamesList AlterExpressionConstraintTail | 'NOT'? 'ENFORCED' | CheckConstraintSpec ) )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionDrop -====================================================================================================================== - - -.. raw:: html - - - - - - DROP - - PARTITION - - PartitionNamesList - - ColumnsNamesList - COLUMN - - IF - - EXISTS - - KeywordOrIdentifier - INVALIDATE - - CASCADE - - CONSTRAINTS - - INDEX - - KEY - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - UNIQUE - - FOREIGN - - KEY - - ColumnsNamesList - PRIMARY - - KEY - - CONSTRAINT - - IF - - EXISTS - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - CASCADE - - RESTRICT - - -
- - -
         ::= 'DROP' ( 'PARTITION' PartitionNamesList | ( ColumnsNamesList | 'COLUMN'? ( 'IF' 'EXISTS' )? KeywordOrIdentifier ) 'INVALIDATE'? ( 'CASCADE' 'CONSTRAINTS'? )? | ( 'INDEX' | 'KEY' ) ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) | ( ( 'UNIQUE' | 'FOREIGN' 'KEY' ) ColumnsNamesList | 'PRIMARY' 'KEY' | 'CONSTRAINT' ( 'IF' 'EXISTS' )? ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ) ( 'CASCADE' | 'RESTRICT' )? )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionPartitionOp -====================================================================================================================== - - -.. raw:: html - - - - - - TRUNCATE - - ANALYZE - - CHECK - - OPTIMIZE - - REBUILD - - REPAIR - - PARTITION - - PartitionNamesList - COALESCE - - PARTITION - - S_LONG - REORGANIZE - - PARTITION - - PartitionNamesList - INTO - - PARTITION - - BY - - RANGE - - ( - - Expression - ) - - COLUMNS - - ColumnsNamesList - - PartitionDefinitions - EXCHANGE - - PARTITION - - PartitionNamesList - WITH - - TABLE - - S_IDENTIFIER - WITH - - WITHOUT - - VALIDATION - - REMOVE - - PARTITIONING - - -
- - -
         ::= ( 'TRUNCATE' | 'ANALYZE' | 'CHECK' | 'OPTIMIZE' | 'REBUILD' | 'REPAIR' - ) 'PARTITION' PartitionNamesList
-
           | 'COALESCE' 'PARTITION' S_LONG
-
           | ( 'REORGANIZE' 'PARTITION' PartitionNamesList 'INTO' | 'PARTITION' 'BY' 'RANGE' ( '(' Expression ')' | 'COLUMNS' ColumnsNamesList ) ) PartitionDefinitions
-
           | 'EXCHANGE' 'PARTITION' PartitionNamesList 'WITH' 'TABLE' S_IDENTIFIER ( ( 'WITH' | 'WITHOUT' ) 'VALIDATION' )?
-
           | 'REMOVE' 'PARTITIONING'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionAddAlterModify -====================================================================================================================== - - -.. raw:: html - - - - - - ADD - - ALTER - - MODIFY - - PRIMARY - - KEY - - ColumnsNamesList - - AlterExpressionConstraintState - - AlterExpressionUsingIndex - KEY - - INDEX - - RelObjectName - - UsingIndexType - - IndexColumnsWithParamsList - - IndexOptionList - - AlterExpressionConstraintState - SPATIAL - - FULLTEXT - - INDEX - - KEY - - RelObjectName - - ColumnsNamesList - - IndexOptionList - - RelObjectName - COMMENT - - S_CHAR_LITERAL - PARTITION - - PartitionDefinitions - COLUMN - - COLUMNS - - IF - - NOT - - EXISTS - - AlterExpressionColumnChanges - - AlterExpressionColumnDataType - - AlterExpressionColumnDropNotNull - - AlterExpressionColumnChanges - UNIQUE - - KEY - - INDEX - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - ColumnsNamesList - - AlterExpressionUsingIndex - - IndexWithComment - - ForeignKeySpec - CHECK - - RelObjectName - NOT - - ENFORCED - - AlterExpressionAddConstraint - -
- - -
         ::= ( 'ADD' | 'ALTER' | 'MODIFY' ) ( 'PRIMARY' 'KEY' ColumnsNamesList AlterExpressionConstraintState AlterExpressionUsingIndex? | ( 'KEY' | 'INDEX' ) RelObjectName? UsingIndexType? IndexColumnsWithParamsList? IndexOptionList AlterExpressionConstraintState | ( 'SPATIAL' | 'FULLTEXT' ) ( 'INDEX' | 'KEY' )? RelObjectName? ColumnsNamesList IndexOptionList | RelObjectName 'COMMENT' S_CHAR_LITERAL | 'PARTITION' PartitionDefinitions | ( 'COLUMN' | 'COLUMNS' )? ( 'IF' 'NOT' 'EXISTS' )? ( AlterExpressionColumnChanges | AlterExpressionColumnDataType | AlterExpressionColumnDropNotNull ) | AlterExpressionColumnChanges | 'UNIQUE' ( 'KEY' | 'INDEX' )? ( S_IDENTIFIER | S_QUOTED_IDENTIFIER )? ColumnsNamesList AlterExpressionUsingIndex? IndexWithComment? | ForeignKeySpec | 'CHECK' RelObjectName 'NOT'? 'ENFORCED' | AlterExpressionAddConstraint )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionRenameOp -====================================================================================================================== - - -.. raw:: html - - - - - - RENAME - - INDEX - - KEY - - CONSTRAINT - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - TO - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - COLUMN - - KeywordOrIdentifier - TO - - KeywordOrIdentifier - -
- - -
         ::= 'RENAME' ( ( ( 'INDEX' | 'KEY' | 'CONSTRAINT' ) ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) )? 'TO' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) | 'COLUMN'? KeywordOrIdentifier 'TO' KeywordOrIdentifier )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpression -====================================================================================================================== - - -.. raw:: html - - - - - - AlterExpressionAddAlterModify - CHANGE - - COLUMN - - KeywordOrIdentifier - - AlterExpressionColumnDataType - - AlterExpressionDrop - FORCE - - ROW - - LEVEL - - SECURITY - - NO - - FORCE - - ROW - - LEVEL - - SECURITY - - ALGORITHM - - LOCK - - ENGINE - - = - - RelObjectName - KEY_BLOCK_SIZE - - AUTO_INCREMENT - - = - - S_LONG - - AlterExpressionRenameOp - CONVERT - - TO - - CHARACTER - - SET - - S_IDENTIFIER - COLLATE - - DEFAULT - - CHARACTER - - SET - - = - - S_IDENTIFIER - COLLATE - - COLLATE - - = - - S_IDENTIFIER - COMMENT - - ENCRYPTION - - = - - S_CHAR_LITERAL - - AlterExpressionDiscardOrImport - DISABLE - - ENABLE - - ROW - - LEVEL - - SECURITY - - KEYS - - AlterExpressionPartitionOp - - captureRest - -
- - -
         ::= AlterExpressionAddAlterModify
-
           | 'CHANGE' 'COLUMN'? KeywordOrIdentifier AlterExpressionColumnDataType
-
           | AlterExpressionDrop
-
           | 'FORCE' ( 'ROW' 'LEVEL' 'SECURITY' )?
-
           | 'NO' 'FORCE' 'ROW' 'LEVEL' 'SECURITY'
-
           | ( 'ALGORITHM' | 'LOCK' | 'ENGINE' ) '='? RelObjectName
-
           | ( 'KEY_BLOCK_SIZE' | 'AUTO_INCREMENT' ) '='? S_LONG
-
           | AlterExpressionRenameOp
-
           | ( 'CONVERT' 'TO' 'CHARACTER' 'SET' ( S_IDENTIFIER 'COLLATE' )? | 'DEFAULT'? ( 'CHARACTER' 'SET' ( '='? S_IDENTIFIER 'COLLATE' )? | 'COLLATE' ) '='? ) S_IDENTIFIER
-
           | ( 'COMMENT' | 'ENCRYPTION' ) '='? S_CHAR_LITERAL
-
           | AlterExpressionDiscardOrImport
-
           | ( 'DISABLE' | 'ENABLE' ) ( 'ROW' 'LEVEL' 'SECURITY' | 'KEYS' )
-
           | AlterExpressionPartitionOp
-
           | captureRest
-
- Referenced by: -
- - -====================================================================================================================== -Alter -====================================================================================================================== - - -.. raw:: html - - - - - - ALTER - - AlterTable - - AlterSession - - AlterView - - AlterSystemStatement - - AlterSequence - - captureRest - REPLACE - - AlterView - - captureRest - -
- - -
           | 'REPLACE' ( AlterView | captureRest )
-
- Referenced by: -
- - -====================================================================================================================== -AlterTable -====================================================================================================================== - - -.. raw:: html - - - - - - TABLE - - ONLY - - IF - - EXISTS - - Table - - AlterExpression - , - - -
- - -
         ::= 'TABLE' 'ONLY'? ( 'IF' 'EXISTS' )? Table AlterExpression ( ',' AlterExpression )*
-
- Referenced by: -
- - -====================================================================================================================== -AlterSession -====================================================================================================================== - - -.. raw:: html - - - - - - SESSION - - ADVISE - - COMMIT - - ROLLBACK - - NOTHING - - CLOSE - - DATABASE - - LINK - - ENABLE - - DISABLE - - COMMIT - - IN - - PROCEDURE - - GUARD - - PARALLEL - - DML - - DDL - - QUERY - - RESUMABLE - - FORCE - - PARALLEL - - DML - - DDL - - QUERY - - SET - - S_CHAR_LITERAL - - S_IDENTIFIER - = - - S_LONG - PARALLEL - - -
- - -
         ::= 'SESSION' ( 'ADVISE' ( 'COMMIT' | 'ROLLBACK' | 'NOTHING' ) | 'CLOSE' - 'DATABASE' 'LINK' | ( 'ENABLE' | 'DISABLE' ) ( 'COMMIT' 'IN' 'PROCEDURE' | 'GUARD' - | 'PARALLEL' ( 'DML' | 'DDL' | 'QUERY' ) | 'RESUMABLE' ) | 'FORCE' 'PARALLEL' ( 'DML' - | 'DDL' | 'QUERY' ) | 'SET' ) ( S_CHAR_LITERAL | S_IDENTIFIER | '=' | S_LONG | 'PARALLEL' )*
-
- Referenced by: -
- - -====================================================================================================================== -AlterSystemStatement -====================================================================================================================== - - -.. raw:: html - - - - - - SYSTEM - - ARCHIVE - - LOG - - CHECKPOINT - - DUMP - - ACTIVE - - SESSION - - HISTORY - - ENABLE - - DISABLE - - DISTRIBUTED - - RECOVERY - - RESTRICTED - - SESSION - - FLUSH - - DISCONNECT - - KILL - - SESSION - - SWITCH - - SUSPEND - - RESUME - - QUIESCE - - RESTRICTED - - UNQIESCE - - SHUTDOWN - - REGISTER - - SET - - RESET - - captureRest - -
- - -
         ::= 'SYSTEM' ( 'ARCHIVE' 'LOG' | 'CHECKPOINT' | 'DUMP' 'ACTIVE' 'SESSION' - 'HISTORY' | ( 'ENABLE' | 'DISABLE' ) ( 'DISTRIBUTED' 'RECOVERY' | 'RESTRICTED' 'SESSION' - ) | 'FLUSH' | ( 'DISCONNECT' | 'KILL' ) 'SESSION' | 'SWITCH' | 'SUSPEND' | 'RESUME' - | 'QUIESCE' 'RESTRICTED' | 'UNQIESCE' | 'SHUTDOWN' | 'REGISTER' | 'SET' | 'RESET' - ) captureRest
-
- Referenced by: -
- - -====================================================================================================================== -Wait -====================================================================================================================== - - -.. raw:: html - - - - - - WAIT - - S_LONG - -
- -
Wait     ::= 'WAIT' S_LONG
-
- Referenced by: -
- - -====================================================================================================================== -SavepointStatement -====================================================================================================================== - - -.. raw:: html - - - - - - SAVEPOINT - - S_IDENTIFIER - -
- - -
         ::= 'SAVEPOINT' S_IDENTIFIER
-
- Referenced by: -
- - -====================================================================================================================== -RollbackStatement -====================================================================================================================== - - -.. raw:: html - - - - - - ROLLBACK - - WORK - - TO - - SAVEPOINT - - S_IDENTIFIER - FORCE - - S_CHAR_LITERAL - -
- - -
         ::= 'ROLLBACK' 'WORK'? ( 'TO' 'SAVEPOINT'? S_IDENTIFIER | 'FORCE' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -Comment -====================================================================================================================== - - -.. raw:: html - - - - - - COMMENT - - ON - - TABLE - - VIEW - - Table - COLUMN - - Column - IS - - S_CHAR_LITERAL - -
- -
Comment  ::= 'COMMENT' 'ON' ( ( 'TABLE' | 'VIEW' ) Table | 'COLUMN' Column ) 'IS' S_CHAR_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -Grant -====================================================================================================================== - - -.. raw:: html - - - - - - GRANT - - readGrantTypes - , - - ON - - RelObjectNames - - S_IDENTIFIER - TO - - UsersList - -
- -
Grant    ::= 'GRANT' ( ( readGrantTypes ( ',' readGrantTypes )* )? 'ON' RelObjectNames | S_IDENTIFIER ) 'TO' UsersList
-
- Referenced by: -
- - -====================================================================================================================== -UsersList -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - , - - ColumnsNamesListItem - -
- - -
         ::= RelObjectName ( ',' ColumnsNamesListItem )*
-
- Referenced by: -
- - -====================================================================================================================== -readGrantTypes -====================================================================================================================== - - -.. raw:: html - - - - - - K_SELECT - INSERT - - UPDATE - - DELETE - - EXECUTE - - ALTER - - DROP - - -
- - -
         ::= K_SELECT
-
           | 'INSERT'
-
           | 'UPDATE'
-
           | 'DELETE'
-
           | 'EXECUTE'
-
           | 'ALTER'
-
           | 'DROP'
-
- Referenced by: -
- - -====================================================================================================================== -Sequence -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNames - -
- - -
- Referenced by: -
- - -====================================================================================================================== -SequenceParameters -====================================================================================================================== - - -.. raw:: html - - - - - - INCREMENT - - BY - - START - - WITH - - MAXVALUE - - MINVALUE - - CACHE - - S_LONG - RESTART - - WITH - - S_LONG - NOMAXVALUE - - NOMINVALUE - - NOCYCLE - - CYCLE - - NOCACHE - - ORDER - - NOORDER - - KEEP - - NOKEEP - - SESSION - - GLOBAL - - -
- - -
         ::= ( ( 'INCREMENT' 'BY'? | 'START' 'WITH'? | 'MAXVALUE' | 'MINVALUE' | 'CACHE' - ) S_LONG | 'RESTART' ( 'WITH' S_LONG )? | 'NOMAXVALUE' | 'NOMINVALUE' | 'NOCYCLE' | 'CYCLE' | 'NOCACHE' | 'ORDER' | 'NOORDER' - | 'KEEP' | 'NOKEEP' | 'SESSION' | 'GLOBAL' )*
-
- Referenced by: -
- - -====================================================================================================================== -CreateSequence -====================================================================================================================== - - -.. raw:: html - - - - - - SEQUENCE - - Sequence - AS - - S_IDENTIFIER - - DATA_TYPE - - SequenceParameters - -
- - -
         ::= 'SEQUENCE' Sequence ( 'AS' ( S_IDENTIFIER | DATA_TYPE ) )? SequenceParameters
-
- Referenced by: -
- - -====================================================================================================================== -AlterSequence -====================================================================================================================== - - -.. raw:: html - - - - - - SEQUENCE - - Sequence - - SequenceParameters - -
- - -
         ::= 'SEQUENCE' Sequence SequenceParameters
-
- Referenced by: -
- - -====================================================================================================================== -Create -====================================================================================================================== - - -.. raw:: html - - - - - - CREATE - - OR - - REPLACE - - CreateFunctionStatement - - CreateSchema - - CreateSequence - - CreateSynonym - - CreateTable - - CreateView - - CreatePolicy - TRIGGER - - DOMAIN - - captureRest - - CreateIndex - -
- -
Create   ::= 'CREATE' ( 'OR' 'REPLACE' )? ( CreateFunctionStatement | CreateSchema | CreateSequence | CreateSynonym | CreateTable | CreateView | CreatePolicy | ( 'TRIGGER' | 'DOMAIN' )? captureRest | CreateIndex )
-
- Referenced by: -
- - -====================================================================================================================== -CreateFunctionStatement -====================================================================================================================== - - -.. raw:: html - - - - - - FUNCTION - - PROCEDURE - - captureFunctionBody - -
- - -
         ::= ( 'FUNCTION' | 'PROCEDURE' ) captureFunctionBody
-
- Referenced by: -
- - -====================================================================================================================== -CreateSynonym -====================================================================================================================== - - -.. raw:: html - - - - - - PUBLIC - - SYNONYM - - Synonym - FOR - - RelObjectNames - -
- - -
         ::= 'PUBLIC'? 'SYNONYM' Synonym 'FOR' RelObjectNames
-
- Referenced by: -
- - -====================================================================================================================== -Synonym -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNames - -
- - -
- Referenced by: -
- - -====================================================================================================================== -CreatePolicy -====================================================================================================================== - - -.. raw:: html - - - - - - POLICY - - RelObjectName - ON - - Table - FOR - - ALL - - K_SELECT - INSERT - - UPDATE - - DELETE - - TO - - RelObjectName - , - - USING - - ( - - Expression - ) - - WITH - - CHECK - - ( - - Expression - ) - - -
- - -
         ::= 'POLICY' RelObjectName 'ON' Table ( 'FOR' ( 'ALL' | K_SELECT | 'INSERT' | 'UPDATE' | 'DELETE' ) )? ( 'TO' RelObjectName ( ',' RelObjectName )* )? ( 'USING' '(' Expression ')' )? ( 'WITH' 'CHECK' '(' Expression ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -UnsupportedStatement -====================================================================================================================== - - -.. raw:: html - - - - - - captureUnsupportedStatementDeclaration - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -IdentifierChain -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNameExt - . - - -
- - -
         ::= RelObjectNameExt ( '.' RelObjectNameExt )*
-
- - -====================================================================================================================== -IdentifierChain2 -====================================================================================================================== - - -.. raw:: html - - - - - - . - - RelObjectNameExt - -
- - -
         ::= ( '.' RelObjectNameExt )*
-
- Referenced by: -
- - -====================================================================================================================== -CharacterPrimary -====================================================================================================================== - - -.. raw:: html - - - - - - TranscodingFunction - - TrimFunction - -
- - -
         ::= TranscodingFunction
-
           | TrimFunction
-
- Referenced by: -
- - -====================================================================================================================== -TranscodingFunction -====================================================================================================================== - - -.. raw:: html - - - - - - TRY_CONVERT - - SAFE_CONVERT - - CONVERT - - ( - - ColDataType - , - - Expression - , - - S_LONG - - Expression - USING - - IdentifierChain - ) - - -
- - -
         ::= ( 'TRY_CONVERT' | 'SAFE_CONVERT' | 'CONVERT' ) '(' ( ColDataType ',' Expression ( ',' S_LONG )? | Expression 'USING' IdentifierChain ) ')'
-
- Referenced by: -
- - -====================================================================================================================== -TrimFunction -====================================================================================================================== - - -.. raw:: html - - - - - - TRIM - - ( - - LEADING - - TRAILING - - BOTH - - Expression - , - - FROM - - Expression - ) - - -
- - -
         ::= 'TRIM' '(' ( 'LEADING' | 'TRAILING' | 'BOTH' )? Expression? ( ( ',' | 'FROM' ) Expression )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -SnowflakeTimeTravelAt -====================================================================================================================== - - -.. raw:: html - - - - - - AT - - ( - - K_DATETIMELITERAL - OFFSET - - => - - Expression - STATEMENT - - => - - S_CHAR_LITERAL - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - STREAM - - => - - S_CHAR_LITERAL - ) - - -
- - -
         ::= 'AT' '(' ( ( K_DATETIMELITERAL | 'OFFSET' ) '=>' Expression | 'STATEMENT' '=>' ( S_CHAR_LITERAL | S_IDENTIFIER | S_QUOTED_IDENTIFIER ) | 'STREAM' '=>' S_CHAR_LITERAL ) ')'
-
- - -====================================================================================================================== -SnowflakeTimeTravelBefore -====================================================================================================================== - - -.. raw:: html - - - - - - BEFORE - - ( - - STATEMENT - - => - - S_CHAR_LITERAL - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - ) - - -
- - -
         ::= 'BEFORE' '(' 'STATEMENT' '=>' ( S_CHAR_LITERAL | S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ')'
-
- - -====================================================================================================================== -SnowflakeTimeTravelChange -====================================================================================================================== - - -.. raw:: html - - - - - - CHANGES - - ( - - INFORMATION - - => - - DEFAULT - - APPEND_ONLY - - ) - - SnowflakeTimeTravelAt - - SnowflakeTimeTravelBefore - END - - ( - - K_DATETIMELITERAL - OFFSET - - => - - Expression - STATEMENT - - => - - S_CHAR_LITERAL - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - ) - - -
- - -
         ::= 'CHANGES' '(' 'INFORMATION' '=>' ( 'DEFAULT' | 'APPEND_ONLY' ) ')' ( - SnowflakeTimeTravelAt | SnowflakeTimeTravelBefore ) ( 'END' '(' ( ( K_DATETIMELITERAL | 'OFFSET' ) '=>' Expression | 'STATEMENT' '=>' ( S_CHAR_LITERAL | S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ) ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -DataBricksTemporalSpec -====================================================================================================================== - - -.. raw:: html - - - - - - @ - - @V - - S_CHAR_LITERAL - - S_LONG - FOR - - SYSTEM_TIMESTAMP - - K_DATETIMELITERAL - AS - - OF - - Expression - SYSTEM_VERSION - - VERSION - - AS - - OF - - S_LONG - - S_CHAR_LITERAL - -
- - -
         ::= ( '@' | '@V' ) ( S_CHAR_LITERAL | S_LONG )
-
           | 'FOR'? ( 'SYSTEM_TIMESTAMP' | K_DATETIMELITERAL ) 'AS' 'OF' Expression
-
           | ( 'SYSTEM_VERSION' | 'VERSION' ) 'AS' 'OF' ( S_LONG | S_CHAR_LITERAL )
-
- Referenced by: -
- - -====================================================================================================================== -BigQueryHistoricalVersion -====================================================================================================================== - - -.. raw:: html - - - - - - FOR - - SYSTEM_TIME - - AS - - OF - - Expression - -
- - -
         ::= 'FOR' 'SYSTEM_TIME' 'AS' 'OF' Expression
-
- Referenced by: -
- - -====================================================================================================================== -TimeTravelBeforeAlias -====================================================================================================================== - - -.. raw:: html - - - - - - SnowflakeTimeTravelAt - - SnowflakeTimeTravelBefore - - SnowflakeTimeTravelChange - - DataBricksTemporalSpec - -
- - -
         ::= SnowflakeTimeTravelAt
-
           | SnowflakeTimeTravelBefore
-
           | SnowflakeTimeTravelChange
-
           | DataBricksTemporalSpec
-
- Referenced by: -
- - -====================================================================================================================== -TimeTravelAfterAlias -====================================================================================================================== - - -.. raw:: html - - - - - - BigQueryHistoricalVersion - -
- - -
         ::= BigQueryHistoricalVersion
-
- Referenced by: -
- - -====================================================================================================================== -WHITESPACE -====================================================================================================================== - - -.. raw:: html - - - - - - - - [#x9] - - [#xD] - - [#xA] - - -
- - -
         ::= [ #x9#xD#xA]
-
- - -====================================================================================================================== -K_ISOLATION -====================================================================================================================== - - -.. raw:: html - - - - - - UR - - RS - - RR - - CS - - -
- - -
         ::= 'UR'
-
           | 'RS'
-
           | 'RR'
-
           | 'CS'
-
- Referenced by: -
- - -====================================================================================================================== -K_NEXTVAL -====================================================================================================================== - - -.. raw:: html - - - - - - NEXTVAL - - - - FOR - - NEXT - - - - VALUE - - - - FOR - - -
- - -
         ::= 'NEXTVAL' ( ' '+ 'FOR' )?
-
           | 'NEXT' ' '+ 'VALUE' ' '+ 'FOR'
-
- - -====================================================================================================================== -K_TEXT_LITERAL -====================================================================================================================== - - -.. raw:: html - - - - - - TEXT - - TINYTEXT - - MEDIUMTEXT - - LONGTEXT - - -
- - -
         ::= 'TEXT'
-
           | 'TINYTEXT'
-
           | 'MEDIUMTEXT'
-
           | 'LONGTEXT'
-
- Referenced by: -
- - -====================================================================================================================== -K_TIME_KEY_EXPR -====================================================================================================================== - - -.. raw:: html - - - - - - CURRENT - - _ - - - - TIMESTAMP - - TIME - - DATE - - TIMEZONE - - () - - -
- - -
         ::= 'CURRENT' ( '_' | ' '+ ) ( 'TIMESTAMP' | 'TIME' | 'DATE' | 'TIMEZONE' - ) '()'?
-
- - -====================================================================================================================== -K_STRING_FUNCTION_NAME -====================================================================================================================== - - -.. raw:: html - - - - - - SUBSTR - - SUBSTRING - - TRIM - - POSITION - - OVERLAY - - -
- - -
         ::= 'SUBSTR'
-
           | 'SUBSTRING'
-
           | 'TRIM'
-
           | 'POSITION'
-
           | 'OVERLAY'
-
- - -====================================================================================================================== -K_DATETIMELITERAL -====================================================================================================================== - - -.. raw:: html - - - - - - DATE - - DATETIME - - TIME - - TIMESTAMP - - TIMESTAMPTZ - - -
- - -
         ::= 'DATE'
-
           | 'DATETIME'
-
           | 'TIME'
-
           | 'TIMESTAMP'
-
           | 'TIMESTAMPTZ'
-
- - -====================================================================================================================== -K_DATE_LITERAL -====================================================================================================================== - - -.. raw:: html - - - - - - YEAR - - MONTH - - DAY - - HOUR - - MINUTE - - SECOND - - -
- - -
         ::= 'YEAR'
-
           | 'MONTH'
-
           | 'DAY'
-
           | 'HOUR'
-
           | 'MINUTE'
-
           | 'SECOND'
-
- - -====================================================================================================================== -K_SELECT -====================================================================================================================== - - -.. raw:: html - - - - - - SELECT - - SEL - - -
- -
K_SELECT ::= 'SELECT'
-
           | 'SEL'
-
- - -====================================================================================================================== -K_SIMILAR_TO -====================================================================================================================== - - -.. raw:: html - - - - - - SIMILAR - - - - TO - - -
- - -
         ::= 'SIMILAR' ' '+ 'TO'
-
- Referenced by: -
- - -====================================================================================================================== -ST_SEMICOLON -====================================================================================================================== - - -.. raw:: html - - - - - - ; - - [#xA] - - / - - [#xA] - - go - - [#xA] - - -
- - -
         ::= ';'
-
           | #xA ( [/#xA] | 'go' ) #xA
-
- Referenced by: -
- - -====================================================================================================================== -OP_GREATERTHANEQUALS -====================================================================================================================== - - -.. raw:: html - - - - - - > - - WHITESPACE - = - - -
- - -
         ::= '>' WHITESPACE* '='
-
- Referenced by: -
- - -====================================================================================================================== -OP_MINORTHANEQUALS -====================================================================================================================== - - -.. raw:: html - - - - - - < - - WHITESPACE - = - - -
- - -
         ::= '<' WHITESPACE* '='
-
- Referenced by: -
- - -====================================================================================================================== -OP_NOTEQUALSSTANDARD -====================================================================================================================== - - -.. raw:: html - - - - - - < - - WHITESPACE - > - - -
- - -
         ::= '<' WHITESPACE* '>'
-
- Referenced by: -
- - -====================================================================================================================== -OP_NOTEQUALSBANG -====================================================================================================================== - - -.. raw:: html - - - - - - ! - - WHITESPACE - = - - -
- - -
         ::= '!' WHITESPACE* '='
-
- Referenced by: -
- - -====================================================================================================================== -OP_NOTEQUALSHAT -====================================================================================================================== - - -.. raw:: html - - - - - - ^ - - WHITESPACE - = - - -
- - -
         ::= '^' WHITESPACE* '='
-
- Referenced by: -
- - -====================================================================================================================== -OP_CONCAT -====================================================================================================================== - - -.. raw:: html - - - - - - | - - WHITESPACE - | - - -
- - -
         ::= '|' WHITESPACE* '|'
-
- - -====================================================================================================================== -DT_ZONE -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATETIMELITERAL - - WHITESPACE - ( - - S_LONG - ) - - WHITESPACE - WITH - - WITHOUT - - WHITESPACE - LOCAL - - WHITESPACE - TIME - - WHITESPACE - ZONE - - -
- -
DT_ZONE  ::= K_DATETIMELITERAL WHITESPACE* ( '(' S_LONG ')' )? WHITESPACE* ( 'WITH' | 'WITHOUT' ) WHITESPACE+ ( 'LOCAL' WHITESPACE+ )? 'TIME' WHITESPACE+ 'ZONE'
-
- - -====================================================================================================================== -DATA_TYPE -====================================================================================================================== - - -.. raw:: html - - - - - - BISTRING - - TYPE_BLOB - - TYPE_BOOLEAN - ENUM - - TYPE_REAL - - TYPE_DOUBLE - UUID - - MAP - - TYPE_TINYINT - - TYPE_SMALLINT - - TYPE_INTEGER - - TYPE_BIGINT - HUGEINT - - UTINYINT - - USMALLINT - - UINTEGER - - UBIGINT - - UHUGEINT - - TYPE_DECIMAL - - TYPE_VARCHAR - TIMETZ - - TYPE_TIMESTAMP - -
- - -
         ::= 'BISTRING'
-
           | TYPE_BLOB
-
           | TYPE_BOOLEAN
-
           | 'ENUM'
-
           | TYPE_REAL
-
           | TYPE_DOUBLE
-
           | 'UUID'
-
           | 'MAP'
-
           | TYPE_TINYINT
-
           | TYPE_SMALLINT
-
           | TYPE_INTEGER
-
           | TYPE_BIGINT
-
           | 'HUGEINT'
-
           | 'UTINYINT'
-
           | 'USMALLINT'
-
           | 'UINTEGER'
-
           | 'UBIGINT'
-
           | 'UHUGEINT'
-
           | TYPE_DECIMAL
-
           | TYPE_VARCHAR
-
           | 'TIMETZ'
-
           | TYPE_TIMESTAMP
-
- - -====================================================================================================================== -TYPE_BLOB -====================================================================================================================== - - -.. raw:: html - - - - - - BLOB - - BYTEA - - BINARY - - VARBINARY - - BYTES - - -
- - -
         ::= 'BLOB'
-
           | 'BYTEA'
-
           | 'BINARY'
-
           | 'VARBINARY'
-
           | 'BYTES'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_BOOLEAN -====================================================================================================================== - - -.. raw:: html - - - - - - BOOLEAN - - BOOL - - -
- - -
         ::= 'BOOLEAN'
-
           | 'BOOL'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_DECIMAL -====================================================================================================================== - - -.. raw:: html - - - - - - DECIMAL - - NUMBER - - NUMERIC - - -
- - -
         ::= 'DECIMAL'
-
           | 'NUMBER'
-
           | 'NUMERIC'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_TINYINT -====================================================================================================================== - - -.. raw:: html - - - - - - TINYINT - - INT1 - - -
- - -
         ::= 'TINYINT'
-
           | 'INT1'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_SMALLINT -====================================================================================================================== - - -.. raw:: html - - - - - - SMALLINT - - INT2 - - SHORT - - -
- - -
         ::= 'SMALLINT'
-
           | 'INT2'
-
           | 'SHORT'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_INTEGER -====================================================================================================================== - - -.. raw:: html - - - - - - INTEGER - - INT - - INT4 - - SIGNED - - UNSIGNED - - -
- - -
         ::= 'INTEGER'
-
           | 'INT'
-
           | 'INT4'
-
           | 'SIGNED'
-
           | 'UNSIGNED'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_BIGINT -====================================================================================================================== - - -.. raw:: html - - - - - - BIGINT - - INT8 - - LONG - - -
- - -
         ::= 'BIGINT'
-
           | 'INT8'
-
           | 'LONG'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_REAL -====================================================================================================================== - - -.. raw:: html - - - - - - REAL - - FLOAT4 - - FLOAT - - -
- - -
         ::= 'REAL'
-
           | 'FLOAT4'
-
           | 'FLOAT'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_DOUBLE -====================================================================================================================== - - -.. raw:: html - - - - - - DOUBLE - - PRECISION - - FLOAT8 - - FLOAT64 - - -
- - -
         ::= 'DOUBLE'
-
           | 'PRECISION'
-
           | 'FLOAT8'
-
           | 'FLOAT64'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_VARCHAR -====================================================================================================================== - - -.. raw:: html - - - - - - NVARCHAR - - VARCHAR - - NCHAR - - CHAR - - BPCHAR - - TEXT - - STRING - - CHARACTER - - VARYING - - -
- - -
         ::= 'NVARCHAR'
-
           | 'VARCHAR'
-
           | 'NCHAR'
-
           | 'CHAR'
-
           | 'BPCHAR'
-
           | 'TEXT'
-
           | 'STRING'
-
           | 'CHARACTER'
-
           | 'VARYING'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_TIMESTAMP -====================================================================================================================== - - -.. raw:: html - - - - - - TIMESTAMP_NS - - TIMESTAMP_MS - - TIMESTAMP_S - - -
- - -
         ::= 'TIMESTAMP_NS'
-
           | 'TIMESTAMP_MS'
-
           | 'TIMESTAMP_S'
-
- Referenced by: -
- - -====================================================================================================================== -S_DOUBLE -====================================================================================================================== - - -.. raw:: html - - - - - - S_LONG - . - - S_LONG - e - - E - - + - - [#x2D] - - S_LONG - - S_LONG - . - - e - - E - - + - - [#x2D] - - S_LONG - e - - E - - + - - [#x2D] - - S_LONG - -
- -
S_DOUBLE ::= S_LONG? '.' S_LONG ( [eE] [+#x2D]? S_LONG )?
-
           | S_LONG ( '.' ( [eE] [+#x2D]? S_LONG )? | [eE] [+#x2D]? S_LONG )
-
- - -====================================================================================================================== -S_LONG -====================================================================================================================== - - -.. raw:: html - - - - - - DIGIT - -
- -
S_LONG   ::= DIGIT+
-
- - -====================================================================================================================== -DIGIT -====================================================================================================================== - - -.. raw:: html - - - - - - [0-9] - - -
- -
DIGIT    ::= [0-9]
-
- Referenced by: -
- - -====================================================================================================================== -S_HEX -====================================================================================================================== - - -.. raw:: html - - - - - - X - - ' - - HEX_VALUE - ' - - - - 0x - - HEX_VALUE - -
- -
S_HEX    ::= 'X' ( "'" HEX_VALUE* "'" ' '* )+
-
           | '0x' HEX_VALUE+
-
- - -====================================================================================================================== -HEX_VALUE -====================================================================================================================== - - -.. raw:: html - - - - - - [0-9] - - [A-F] - - - - -
- - -
         ::= [0-9A-F ]
-
- Referenced by: -
- - -====================================================================================================================== -LINE_COMMENT -====================================================================================================================== - - -.. raw:: html - - - - - - -- - - // - - [^#xD#xA] - - -
- - -
         ::= ( '--' | '//' ) [^#xD#xA]*
-
- Not referenced by any. -
- - -====================================================================================================================== -MULTI_LINE_COMMENT -====================================================================================================================== - - -.. raw:: html - - - - - - . - - -
- - -
         ::= .
-
- Not referenced by any. -
- - -====================================================================================================================== -S_PARAMETER -====================================================================================================================== - - -.. raw:: html - - - - - - $ - - [0-9] - - -
- - -
         ::= '$' [0-9]+
-
- Referenced by: -
- - -====================================================================================================================== -S_IDENTIFIER -====================================================================================================================== - - -.. raw:: html - - - - - - LETTER - - PART_LETTER - -
- - -
         ::= LETTER PART_LETTER*
-
- - -====================================================================================================================== -LETTER -====================================================================================================================== - - -.. raw:: html - - - - - - UnicodeIdentifierStart - - Nd - $ - - _ - - [#x23] - - -
- - -
           | Nd
-
           | [$_#x23]
-
- Referenced by: -
- - -====================================================================================================================== -PART_LETTER -====================================================================================================================== - - -.. raw:: html - - - - - - UnicodeIdentifierStart - - UnicodeIdentifierExtend - $ - - _ - - @ - - [#x23] - - -
- - -
         ::= UnicodeIdentifierStart
-
           | UnicodeIdentifierExtend
-
           | [$_@#x23]
-
- Referenced by: -
- - -====================================================================================================================== -S_AT_IDENTIFIER -====================================================================================================================== - - -.. raw:: html - - - - - - @ - - @ - - S_IDENTIFIER - -
- - -
         ::= '@' '@'? S_IDENTIFIER
-
- Referenced by: -
- - -====================================================================================================================== -UnicodeIdentifierStart -====================================================================================================================== - - -.. raw:: html - - - - - - [#xB7] - - Ll - - Lm - - Lo - - Lt - - Lu - - Nl - - CJK - -
- - -
         ::= #xB7
-
           | Ll
-
           | Lm
-
           | Lo
-
           | Lt
-
           | Lu
-
           | Nl
-
           | CJK
-
- Referenced by: -
- - -====================================================================================================================== -Ll -====================================================================================================================== - - -.. raw:: html - - - - - - [a-z] - - [#xB5] - - [#xDF-#xF6] - - [#xF8-#xFF] - - [#x101] - - [#x103] - - [#x105] - - [#x107] - - [#x109] - - [#x10B] - - [#x10D] - - [#x10F] - - [#x111] - - [#x113] - - [#x115] - - [#x117] - - [#x119] - - [#x11B] - - [#x11D] - - [#x11F] - - [#x121] - - [#x123] - - [#x125] - - [#x127] - - [#x129] - - [#x12B] - - [#x12D] - - [#x12F] - - [#x131] - - [#x133] - - [#x135] - - [#x137-#x138] - - [#x13A] - - [#x13C] - - [#x13E] - - [#x140] - - [#x142] - - [#x144] - - [#x146] - - [#x148-#x149] - - [#x14B] - - [#x14D] - - [#x14F] - - [#x151] - - [#x153] - - [#x155] - - [#x157] - - [#x159] - - [#x15B] - - [#x15D] - - [#x15F] - - [#x161] - - [#x163] - - [#x165] - - [#x167] - - [#x169] - - [#x16B] - - [#x16D] - - [#x16F] - - [#x171] - - [#x173] - - [#x175] - - [#x177] - - [#x17A] - - [#x17C] - - [#x17E-#x180] - - [#x183] - - [#x185] - - [#x188] - - [#x18C-#x18D] - - [#x192] - - [#x195] - - [#x199-#x19B] - - [#x19E] - - [#x1A1] - - [#x1A3] - - [#x1A5] - - [#x1A8] - - [#x1AA-#x1AB] - - [#x1AD] - - [#x1B0] - - [#x1B4] - - [#x1B6] - - [#x1B9-#x1BA] - - [#x1BD-#x1BF] - - [#x1C6] - - [#x1C9] - - [#x1CC] - - [#x1CE] - - [#x1D0] - - [#x1D2] - - [#x1D4] - - [#x1D6] - - [#x1D8] - - [#x1DA] - - [#x1DC-#x1DD] - - [#x1DF] - - [#x1E1] - - [#x1E3] - - [#x1E5] - - [#x1E7] - - [#x1E9] - - [#x1EB] - - [#x1ED] - - [#x1EF-#x1F0] - - [#x1F3] - - [#x1F5] - - [#x1F9] - - [#x1FB] - - [#x1FD] - - [#x1FF] - - [#x201] - - [#x203] - - [#x205] - - [#x207] - - [#x209] - - [#x20B] - - [#x20D] - - [#x20F] - - [#x211] - - [#x213] - - [#x215] - - [#x217] - - [#x219] - - [#x21B] - - [#x21D] - - [#x21F] - - [#x221] - - [#x223] - - [#x225] - - [#x227] - - [#x229] - - [#x22B] - - [#x22D] - - [#x22F] - - [#x231] - - [#x233-#x239] - - [#x23C] - - [#x23F-#x240] - - [#x242] - - [#x247] - - [#x249] - - [#x24B] - - [#x24D] - - [#x24F-#x293] - - [#x295-#x2AF] - - [#x371] - - [#x373] - - [#x377] - - [#x37B-#x37D] - - [#x390] - - [#x3AC-#x3CE] - - [#x3D0-#x3D1] - - [#x3D5-#x3D7] - - [#x3D9] - - [#x3DB] - - [#x3DD] - - [#x3DF] - - [#x3E1] - - [#x3E3] - - [#x3E5] - - [#x3E7] - - [#x3E9] - - [#x3EB] - - [#x3ED] - - [#x3EF-#x3F3] - - [#x3F5] - - [#x3F8] - - [#x3FB-#x3FC] - - [#x430-#x45F] - - [#x461] - - [#x463] - - [#x465] - - [#x467] - - [#x469] - - [#x46B] - - [#x46D] - - [#x46F] - - [#x471] - - [#x473] - - [#x475] - - [#x477] - - [#x479] - - [#x47B] - - [#x47D] - - [#x47F] - - [#x481] - - [#x48B] - - [#x48D] - - [#x48F] - - [#x491] - - [#x493] - - [#x495] - - [#x497] - - [#x499] - - [#x49B] - - [#x49D] - - [#x49F] - - [#x4A1] - - [#x4A3] - - [#x4A5] - - [#x4A7] - - [#x4A9] - - [#x4AB] - - [#x4AD] - - [#x4AF] - - [#x4B1] - - [#x4B3] - - [#x4B5] - - [#x4B7] - - [#x4B9] - - [#x4BB] - - [#x4BD] - - [#x4BF] - - [#x4C2] - - [#x4C4] - - [#x4C6] - - [#x4C8] - - [#x4CA] - - [#x4CC] - - [#x4CE-#x4CF] - - [#x4D1] - - [#x4D3] - - [#x4D5] - - [#x4D7] - - [#x4D9] - - [#x4DB] - - [#x4DD] - - [#x4DF] - - [#x4E1] - - [#x4E3] - - [#x4E5] - - [#x4E7] - - [#x4E9] - - [#x4EB] - - [#x4ED] - - [#x4EF] - - [#x4F1] - - [#x4F3] - - [#x4F5] - - [#x4F7] - - [#x4F9] - - [#x4FB] - - [#x4FD] - - [#x4FF] - - [#x501] - - [#x503] - - [#x505] - - [#x507] - - [#x509] - - [#x50B] - - [#x50D] - - [#x50F] - - [#x511] - - [#x513] - - [#x515] - - [#x517] - - [#x519] - - [#x51B] - - [#x51D] - - [#x51F] - - [#x521] - - [#x523] - - [#x525] - - [#x527] - - [#x529] - - [#x52B] - - [#x52D] - - [#x52F] - - [#x560-#x588] - - [#x10D0-#x10FA] - - [#x10FD-#x10FF] - - [#x13F8-#x13FD] - - [#x1C80-#x1C88] - - [#x1D00-#x1D2B] - - [#x1D6B-#x1D77] - - [#x1D79-#x1D9A] - - [#x1E01] - - [#x1E03] - - [#x1E05] - - [#x1E07] - - [#x1E09] - - [#x1E0B] - - [#x1E0D] - - [#x1E0F] - - [#x1E11] - - [#x1E13] - - [#x1E15] - - [#x1E17] - - [#x1E19] - - [#x1E1B] - - [#x1E1D] - - [#x1E1F] - - [#x1E21] - - [#x1E23] - - [#x1E25] - - [#x1E27] - - [#x1E29] - - [#x1E2B] - - [#x1E2D] - - [#x1E2F] - - [#x1E31] - - [#x1E33] - - [#x1E35] - - [#x1E37] - - [#x1E39] - - [#x1E3B] - - [#x1E3D] - - [#x1E3F] - - [#x1E41] - - [#x1E43] - - [#x1E45] - - [#x1E47] - - [#x1E49] - - [#x1E4B] - - [#x1E4D] - - [#x1E4F] - - [#x1E51] - - [#x1E53] - - [#x1E55] - - [#x1E57] - - [#x1E59] - - [#x1E5B] - - [#x1E5D] - - [#x1E5F] - - [#x1E61] - - [#x1E63] - - [#x1E65] - - [#x1E67] - - [#x1E69] - - [#x1E6B] - - [#x1E6D] - - [#x1E6F] - - [#x1E71] - - [#x1E73] - - [#x1E75] - - [#x1E77] - - [#x1E79] - - [#x1E7B] - - [#x1E7D] - - [#x1E7F] - - [#x1E81] - - [#x1E83] - - [#x1E85] - - [#x1E87] - - [#x1E89] - - [#x1E8B] - - [#x1E8D] - - [#x1E8F] - - [#x1E91] - - [#x1E93] - - [#x1E95-#x1E9D] - - [#x1E9F] - - [#x1EA1] - - [#x1EA3] - - [#x1EA5] - - [#x1EA7] - - [#x1EA9] - - [#x1EAB] - - [#x1EAD] - - [#x1EAF] - - [#x1EB1] - - [#x1EB3] - - [#x1EB5] - - [#x1EB7] - - [#x1EB9] - - [#x1EBB] - - [#x1EBD] - - [#x1EBF] - - [#x1EC1] - - [#x1EC3] - - [#x1EC5] - - [#x1EC7] - - [#x1EC9] - - [#x1ECB] - - [#x1ECD] - - [#x1ECF] - - [#x1ED1] - - [#x1ED3] - - [#x1ED5] - - [#x1ED7] - - [#x1ED9] - - [#x1EDB] - - [#x1EDD] - - [#x1EDF] - - [#x1EE1] - - [#x1EE3] - - [#x1EE5] - - [#x1EE7] - - [#x1EE9] - - [#x1EEB] - - [#x1EED] - - [#x1EEF] - - [#x1EF1] - - [#x1EF3] - - [#x1EF5] - - [#x1EF7] - - [#x1EF9] - - [#x1EFB] - - [#x1EFD] - - [#x1EFF-#x1F07] - - [#x1F10-#x1F15] - - [#x1F20-#x1F27] - - [#x1F30-#x1F37] - - [#x1F40-#x1F45] - - [#x1F50-#x1F57] - - [#x1F60-#x1F67] - - [#x1F70-#x1F7D] - - [#x1F80-#x1F87] - - [#x1F90-#x1F97] - - [#x1FA0-#x1FA7] - - [#x1FB0-#x1FB4] - - [#x1FB6-#x1FB7] - - [#x1FBE] - - [#x1FC2-#x1FC4] - - [#x1FC6-#x1FC7] - - [#x1FD0-#x1FD3] - - [#x1FD6-#x1FD7] - - [#x1FE0-#x1FE7] - - [#x1FF2-#x1FF4] - - [#x1FF6-#x1FF7] - - [#x210A] - - [#x210E-#x210F] - - [#x2113] - - [#x212F] - - [#x2134] - - [#x2139] - - [#x213C-#x213D] - - [#x2146-#x2149] - - [#x214E] - - [#x2184] - - [#x2C30-#x2C5F] - - [#x2C61] - - [#x2C65-#x2C66] - - [#x2C68] - - [#x2C6A] - - [#x2C6C] - - [#x2C71] - - [#x2C73-#x2C74] - - [#x2C76-#x2C7B] - - [#x2C81] - - [#x2C83] - - [#x2C85] - - [#x2C87] - - [#x2C89] - - [#x2C8B] - - [#x2C8D] - - [#x2C8F] - - [#x2C91] - - [#x2C93] - - [#x2C95] - - [#x2C97] - - [#x2C99] - - [#x2C9B] - - [#x2C9D] - - [#x2C9F] - - [#x2CA1] - - [#x2CA3] - - [#x2CA5] - - [#x2CA7] - - [#x2CA9] - - [#x2CAB] - - [#x2CAD] - - [#x2CAF] - - [#x2CB1] - - [#x2CB3] - - [#x2CB5] - - [#x2CB7] - - [#x2CB9] - - [#x2CBB] - - [#x2CBD] - - [#x2CBF] - - [#x2CC1] - - [#x2CC3] - - [#x2CC5] - - [#x2CC7] - - [#x2CC9] - - [#x2CCB] - - [#x2CCD] - - [#x2CCF] - - [#x2CD1] - - [#x2CD3] - - [#x2CD5] - - [#x2CD7] - - [#x2CD9] - - [#x2CDB] - - [#x2CDD] - - [#x2CDF] - - [#x2CE1] - - [#x2CE3-#x2CE4] - - [#x2CEC] - - [#x2CEE] - - [#x2CF3] - - [#x2D00-#x2D25] - - [#x2D27] - - [#x2D2D] - - [#xA641] - - [#xA643] - - [#xA645] - - [#xA647] - - [#xA649] - - [#xA64B] - - [#xA64D] - - [#xA64F] - - [#xA651] - - [#xA653] - - [#xA655] - - [#xA657] - - [#xA659] - - [#xA65B] - - [#xA65D] - - [#xA65F] - - [#xA661] - - [#xA663] - - [#xA665] - - [#xA667] - - [#xA669] - - [#xA66B] - - [#xA66D] - - [#xA681] - - [#xA683] - - [#xA685] - - [#xA687] - - [#xA689] - - [#xA68B] - - [#xA68D] - - [#xA68F] - - [#xA691] - - [#xA693] - - [#xA695] - - [#xA697] - - [#xA699] - - [#xA69B] - - [#xA723] - - [#xA725] - - [#xA727] - - [#xA729] - - [#xA72B] - - [#xA72D] - - [#xA72F-#xA731] - - [#xA733] - - [#xA735] - - [#xA737] - - [#xA739] - - [#xA73B] - - [#xA73D] - - [#xA73F] - - [#xA741] - - [#xA743] - - [#xA745] - - [#xA747] - - [#xA749] - - [#xA74B] - - [#xA74D] - - [#xA74F] - - [#xA751] - - [#xA753] - - [#xA755] - - [#xA757] - - [#xA759] - - [#xA75B] - - [#xA75D] - - [#xA75F] - - [#xA761] - - [#xA763] - - [#xA765] - - [#xA767] - - [#xA769] - - [#xA76B] - - [#xA76D] - - [#xA76F] - - [#xA771-#xA778] - - [#xA77A] - - [#xA77C] - - [#xA77F] - - [#xA781] - - [#xA783] - - [#xA785] - - [#xA787] - - [#xA78C] - - [#xA78E] - - [#xA791] - - [#xA793-#xA795] - - [#xA797] - - [#xA799] - - [#xA79B] - - [#xA79D] - - [#xA79F] - - [#xA7A1] - - [#xA7A3] - - [#xA7A5] - - [#xA7A7] - - [#xA7A9] - - [#xA7AF] - - [#xA7B5] - - [#xA7B7] - - [#xA7B9] - - [#xA7BB] - - [#xA7BD] - - [#xA7BF] - - [#xA7C1] - - [#xA7C3] - - [#xA7C8] - - [#xA7CA] - - [#xA7D1] - - [#xA7D3] - - [#xA7D5] - - [#xA7D7] - - [#xA7D9] - - [#xA7F6] - - [#xA7FA] - - [#xAB30-#xAB5A] - - [#xAB60-#xAB68] - - [#xAB70-#xABBF] - - [#xFB00-#xFB06] - - [#xFB13-#xFB17] - - [#xFF41-#xFF5A] - - -
- -
Ll       ::= [a-z#xB5#xDF-#xF6#xF8-#xFF#x101#x103#x105#x107#x109#x10B#x10D#x10F#x111#x113#x115#x117#x119#x11B#x11D#x11F#x121#x123#x125#x127#x129#x12B#x12D#x12F#x131#x133#x135#x137-#x138#x13A#x13C#x13E#x140#x142#x144#x146#x148-#x149#x14B#x14D#x14F#x151#x153#x155#x157#x159#x15B#x15D#x15F#x161#x163#x165#x167#x169#x16B#x16D#x16F#x171#x173#x175#x177#x17A#x17C#x17E-#x180#x183#x185#x188#x18C-#x18D#x192#x195#x199-#x19B#x19E#x1A1#x1A3#x1A5#x1A8#x1AA-#x1AB#x1AD#x1B0#x1B4#x1B6#x1B9-#x1BA#x1BD-#x1BF#x1C6#x1C9#x1CC#x1CE#x1D0#x1D2#x1D4#x1D6#x1D8#x1DA#x1DC-#x1DD#x1DF#x1E1#x1E3#x1E5#x1E7#x1E9#x1EB#x1ED#x1EF-#x1F0#x1F3#x1F5#x1F9#x1FB#x1FD#x1FF#x201#x203#x205#x207#x209#x20B#x20D#x20F#x211#x213#x215#x217#x219#x21B#x21D#x21F#x221#x223#x225#x227#x229#x22B#x22D#x22F#x231#x233-#x239#x23C#x23F-#x240#x242#x247#x249#x24B#x24D#x24F-#x293#x295-#x2AF#x371#x373#x377#x37B-#x37D#x390#x3AC-#x3CE#x3D0-#x3D1#x3D5-#x3D7#x3D9#x3DB#x3DD#x3DF#x3E1#x3E3#x3E5#x3E7#x3E9#x3EB#x3ED#x3EF-#x3F3#x3F5#x3F8#x3FB-#x3FC#x430-#x45F#x461#x463#x465#x467#x469#x46B#x46D#x46F#x471#x473#x475#x477#x479#x47B#x47D#x47F#x481#x48B#x48D#x48F#x491#x493#x495#x497#x499#x49B#x49D#x49F#x4A1#x4A3#x4A5#x4A7#x4A9#x4AB#x4AD#x4AF#x4B1#x4B3#x4B5#x4B7#x4B9#x4BB#x4BD#x4BF#x4C2#x4C4#x4C6#x4C8#x4CA#x4CC#x4CE-#x4CF#x4D1#x4D3#x4D5#x4D7#x4D9#x4DB#x4DD#x4DF#x4E1#x4E3#x4E5#x4E7#x4E9#x4EB#x4ED#x4EF#x4F1#x4F3#x4F5#x4F7#x4F9#x4FB#x4FD#x4FF#x501#x503#x505#x507#x509#x50B#x50D#x50F#x511#x513#x515#x517#x519#x51B#x51D#x51F#x521#x523#x525#x527#x529#x52B#x52D#x52F#x560-#x588#x10D0-#x10FA#x10FD-#x10FF#x13F8-#x13FD#x1C80-#x1C88#x1D00-#x1D2B#x1D6B-#x1D77#x1D79-#x1D9A#x1E01#x1E03#x1E05#x1E07#x1E09#x1E0B#x1E0D#x1E0F#x1E11#x1E13#x1E15#x1E17#x1E19#x1E1B#x1E1D#x1E1F#x1E21#x1E23#x1E25#x1E27#x1E29#x1E2B#x1E2D#x1E2F#x1E31#x1E33#x1E35#x1E37#x1E39#x1E3B#x1E3D#x1E3F#x1E41#x1E43#x1E45#x1E47#x1E49#x1E4B#x1E4D#x1E4F#x1E51#x1E53#x1E55#x1E57#x1E59#x1E5B#x1E5D#x1E5F#x1E61#x1E63#x1E65#x1E67#x1E69#x1E6B#x1E6D#x1E6F#x1E71#x1E73#x1E75#x1E77#x1E79#x1E7B#x1E7D#x1E7F#x1E81#x1E83#x1E85#x1E87#x1E89#x1E8B#x1E8D#x1E8F#x1E91#x1E93#x1E95-#x1E9D#x1E9F#x1EA1#x1EA3#x1EA5#x1EA7#x1EA9#x1EAB#x1EAD#x1EAF#x1EB1#x1EB3#x1EB5#x1EB7#x1EB9#x1EBB#x1EBD#x1EBF#x1EC1#x1EC3#x1EC5#x1EC7#x1EC9#x1ECB#x1ECD#x1ECF#x1ED1#x1ED3#x1ED5#x1ED7#x1ED9#x1EDB#x1EDD#x1EDF#x1EE1#x1EE3#x1EE5#x1EE7#x1EE9#x1EEB#x1EED#x1EEF#x1EF1#x1EF3#x1EF5#x1EF7#x1EF9#x1EFB#x1EFD#x1EFF-#x1F07#x1F10-#x1F15#x1F20-#x1F27#x1F30-#x1F37#x1F40-#x1F45#x1F50-#x1F57#x1F60-#x1F67#x1F70-#x1F7D#x1F80-#x1F87#x1F90-#x1F97#x1FA0-#x1FA7#x1FB0-#x1FB4#x1FB6-#x1FB7#x1FBE#x1FC2-#x1FC4#x1FC6-#x1FC7#x1FD0-#x1FD3#x1FD6-#x1FD7#x1FE0-#x1FE7#x1FF2-#x1FF4#x1FF6-#x1FF7#x210A#x210E-#x210F#x2113#x212F#x2134#x2139#x213C-#x213D#x2146-#x2149#x214E#x2184#x2C30-#x2C5F#x2C61#x2C65-#x2C66#x2C68#x2C6A#x2C6C#x2C71#x2C73-#x2C74#x2C76-#x2C7B#x2C81#x2C83#x2C85#x2C87#x2C89#x2C8B#x2C8D#x2C8F#x2C91#x2C93#x2C95#x2C97#x2C99#x2C9B#x2C9D#x2C9F#x2CA1#x2CA3#x2CA5#x2CA7#x2CA9#x2CAB#x2CAD#x2CAF#x2CB1#x2CB3#x2CB5#x2CB7#x2CB9#x2CBB#x2CBD#x2CBF#x2CC1#x2CC3#x2CC5#x2CC7#x2CC9#x2CCB#x2CCD#x2CCF#x2CD1#x2CD3#x2CD5#x2CD7#x2CD9#x2CDB#x2CDD#x2CDF#x2CE1#x2CE3-#x2CE4#x2CEC#x2CEE#x2CF3#x2D00-#x2D25#x2D27#x2D2D#xA641#xA643#xA645#xA647#xA649#xA64B#xA64D#xA64F#xA651#xA653#xA655#xA657#xA659#xA65B#xA65D#xA65F#xA661#xA663#xA665#xA667#xA669#xA66B#xA66D#xA681#xA683#xA685#xA687#xA689#xA68B#xA68D#xA68F#xA691#xA693#xA695#xA697#xA699#xA69B#xA723#xA725#xA727#xA729#xA72B#xA72D#xA72F-#xA731#xA733#xA735#xA737#xA739#xA73B#xA73D#xA73F#xA741#xA743#xA745#xA747#xA749#xA74B#xA74D#xA74F#xA751#xA753#xA755#xA757#xA759#xA75B#xA75D#xA75F#xA761#xA763#xA765#xA767#xA769#xA76B#xA76D#xA76F#xA771-#xA778#xA77A#xA77C#xA77F#xA781#xA783#xA785#xA787#xA78C#xA78E#xA791#xA793-#xA795#xA797#xA799#xA79B#xA79D#xA79F#xA7A1#xA7A3#xA7A5#xA7A7#xA7A9#xA7AF#xA7B5#xA7B7#xA7B9#xA7BB#xA7BD#xA7BF#xA7C1#xA7C3#xA7C8#xA7CA#xA7D1#xA7D3#xA7D5#xA7D7#xA7D9#xA7F6#xA7FA#xAB30-#xAB5A#xAB60-#xAB68#xAB70-#xABBF#xFB00-#xFB06#xFB13-#xFB17#xFF41-#xFF5A]
-
- Referenced by: -
- - -====================================================================================================================== -Lm -====================================================================================================================== - - -.. raw:: html - - - - - - [#x2B0-#x2C1] - - [#x2C6-#x2D1] - - [#x2E0-#x2E4] - - [#x2EC] - - [#x2EE] - - [#x374] - - [#x37A] - - [#x559] - - [#x640] - - [#x6E5-#x6E6] - - [#x7F4-#x7F5] - - [#x7FA] - - [#x81A] - - [#x824] - - [#x828] - - [#x8C9] - - [#x971] - - [#xE46] - - [#xEC6] - - [#x10FC] - - [#x17D7] - - [#x1843] - - [#x1AA7] - - [#x1C78-#x1C7D] - - [#x1D2C-#x1D6A] - - [#x1D78] - - [#x1D9B-#x1DBF] - - [#x2071] - - [#x207F] - - [#x2090-#x209C] - - [#x2C7C-#x2C7D] - - [#x2D6F] - - [#x2E2F] - - [#x3005] - - [#x3031-#x3035] - - [#x303B] - - [#x309D-#x309E] - - [#x30FC-#x30FE] - - [#xA015] - - [#xA4F8-#xA4FD] - - [#xA60C] - - [#xA67F] - - [#xA69C-#xA69D] - - [#xA717-#xA71F] - - [#xA770] - - [#xA788] - - [#xA7F2-#xA7F4] - - [#xA7F8-#xA7F9] - - [#xA9CF] - - [#xA9E6] - - [#xAA70] - - [#xAADD] - - [#xAAF3-#xAAF4] - - [#xAB5C-#xAB5F] - - [#xAB69] - - [#xFF70] - - [#xFF9E-#xFF9F] - - -
- -
Lm       ::= [#x2B0-#x2C1#x2C6-#x2D1#x2E0-#x2E4#x2EC#x2EE#x374#x37A#x559#x640#x6E5-#x6E6#x7F4-#x7F5#x7FA#x81A#x824#x828#x8C9#x971#xE46#xEC6#x10FC#x17D7#x1843#x1AA7#x1C78-#x1C7D#x1D2C-#x1D6A#x1D78#x1D9B-#x1DBF#x2071#x207F#x2090-#x209C#x2C7C-#x2C7D#x2D6F#x2E2F#x3005#x3031-#x3035#x303B#x309D-#x309E#x30FC-#x30FE#xA015#xA4F8-#xA4FD#xA60C#xA67F#xA69C-#xA69D#xA717-#xA71F#xA770#xA788#xA7F2-#xA7F4#xA7F8-#xA7F9#xA9CF#xA9E6#xAA70#xAADD#xAAF3-#xAAF4#xAB5C-#xAB5F#xAB69#xFF70#xFF9E-#xFF9F]
-
- Referenced by: -
- - -====================================================================================================================== -Lo -====================================================================================================================== - - -.. raw:: html - - - - - - [#xAA] - - [#xBA] - - [#x1BB] - - [#x1C0-#x1C3] - - [#x294] - - [#x5D0-#x5EA] - - [#x5EF-#x5F2] - - [#x620-#x63F] - - [#x641-#x64A] - - [#x66E-#x66F] - - [#x671-#x6D3] - - [#x6D5] - - [#x6EE-#x6EF] - - [#x6FA-#x6FC] - - [#x6FF] - - [#x710] - - [#x712-#x72F] - - [#x74D-#x7A5] - - [#x7B1] - - [#x7CA-#x7EA] - - [#x800-#x815] - - [#x840-#x858] - - [#x860-#x86A] - - [#x870-#x887] - - [#x889-#x88E] - - [#x8A0-#x8C8] - - [#x904-#x939] - - [#x93D] - - [#x950] - - [#x958-#x961] - - [#x972-#x980] - - [#x985-#x98C] - - [#x98F-#x990] - - [#x993-#x9A8] - - [#x9AA-#x9B0] - - [#x9B2] - - [#x9B6-#x9B9] - - [#x9BD] - - [#x9CE] - - [#x9DC-#x9DD] - - [#x9DF-#x9E1] - - [#x9F0-#x9F1] - - [#x9FC] - - [#xA05-#xA0A] - - [#xA0F-#xA10] - - [#xA13-#xA28] - - [#xA2A-#xA30] - - [#xA32-#xA33] - - [#xA35-#xA36] - - [#xA38-#xA39] - - [#xA59-#xA5C] - - [#xA5E] - - [#xA72-#xA74] - - [#xA85-#xA8D] - - [#xA8F-#xA91] - - [#xA93-#xAA8] - - [#xAAA-#xAB0] - - [#xAB2-#xAB3] - - [#xAB5-#xAB9] - - [#xABD] - - [#xAD0] - - [#xAE0-#xAE1] - - [#xAF9] - - [#xB05-#xB0C] - - [#xB0F-#xB10] - - [#xB13-#xB28] - - [#xB2A-#xB30] - - [#xB32-#xB33] - - [#xB35-#xB39] - - [#xB3D] - - [#xB5C-#xB5D] - - [#xB5F-#xB61] - - [#xB71] - - [#xB83] - - [#xB85-#xB8A] - - [#xB8E-#xB90] - - [#xB92-#xB95] - - [#xB99-#xB9A] - - [#xB9C] - - [#xB9E-#xB9F] - - [#xBA3-#xBA4] - - [#xBA8-#xBAA] - - [#xBAE-#xBB9] - - [#xBD0] - - [#xC05-#xC0C] - - [#xC0E-#xC10] - - [#xC12-#xC28] - - [#xC2A-#xC39] - - [#xC3D] - - [#xC58-#xC5A] - - [#xC5D] - - [#xC60-#xC61] - - [#xC80] - - [#xC85-#xC8C] - - [#xC8E-#xC90] - - [#xC92-#xCA8] - - [#xCAA-#xCB3] - - [#xCB5-#xCB9] - - [#xCBD] - - [#xCDD-#xCDE] - - [#xCE0-#xCE1] - - [#xCF1-#xCF2] - - [#xD04-#xD0C] - - [#xD0E-#xD10] - - [#xD12-#xD3A] - - [#xD3D] - - [#xD4E] - - [#xD54-#xD56] - - [#xD5F-#xD61] - - [#xD7A-#xD7F] - - [#xD85-#xD96] - - [#xD9A-#xDB1] - - [#xDB3-#xDBB] - - [#xDBD] - - [#xDC0-#xDC6] - - [#xE01-#xE30] - - [#xE32-#xE33] - - [#xE40-#xE45] - - [#xE81-#xE82] - - [#xE84] - - [#xE86-#xE8A] - - [#xE8C-#xEA3] - - [#xEA5] - - [#xEA7-#xEB0] - - [#xEB2-#xEB3] - - [#xEBD] - - [#xEC0-#xEC4] - - [#xEDC-#xEDF] - - [#xF00] - - [#xF40-#xF47] - - [#xF49-#xF6C] - - [#xF88-#xF8C] - - [#x1000-#x102A] - - [#x103F] - - [#x1050-#x1055] - - [#x105A-#x105D] - - [#x1061] - - [#x1065-#x1066] - - [#x106E-#x1070] - - [#x1075-#x1081] - - [#x108E] - - [#x1100-#x1248] - - [#x124A-#x124D] - - [#x1250-#x1256] - - [#x1258] - - [#x125A-#x125D] - - [#x1260-#x1288] - - [#x128A-#x128D] - - [#x1290-#x12B0] - - [#x12B2-#x12B5] - - [#x12B8-#x12BE] - - [#x12C0] - - [#x12C2-#x12C5] - - [#x12C8-#x12D6] - - [#x12D8-#x1310] - - [#x1312-#x1315] - - [#x1318-#x135A] - - [#x1380-#x138F] - - [#x1401-#x166C] - - [#x166F-#x167F] - - [#x1681-#x169A] - - [#x16A0-#x16EA] - - [#x16F1-#x16F8] - - [#x1700-#x1711] - - [#x171F-#x1731] - - [#x1740-#x1751] - - [#x1760-#x176C] - - [#x176E-#x1770] - - [#x1780-#x17B3] - - [#x17DC] - - [#x1820-#x1842] - - [#x1844-#x1878] - - [#x1880-#x1884] - - [#x1887-#x18A8] - - [#x18AA] - - [#x18B0-#x18F5] - - [#x1900-#x191E] - - [#x1950-#x196D] - - [#x1970-#x1974] - - [#x1980-#x19AB] - - [#x19B0-#x19C9] - - [#x1A00-#x1A16] - - [#x1A20-#x1A54] - - [#x1B05-#x1B33] - - [#x1B45-#x1B4C] - - [#x1B83-#x1BA0] - - [#x1BAE-#x1BAF] - - [#x1BBA-#x1BE5] - - [#x1C00-#x1C23] - - [#x1C4D-#x1C4F] - - [#x1C5A-#x1C77] - - [#x1CE9-#x1CEC] - - [#x1CEE-#x1CF3] - - [#x1CF5-#x1CF6] - - [#x1CFA] - - [#x2135-#x2138] - - [#x2D30-#x2D67] - - [#x2D80-#x2D96] - - [#x2DA0-#x2DA6] - - [#x2DA8-#x2DAE] - - [#x2DB0-#x2DB6] - - [#x2DB8-#x2DBE] - - [#x2DC0-#x2DC6] - - [#x2DC8-#x2DCE] - - [#x2DD0-#x2DD6] - - [#x2DD8-#x2DDE] - - [#x3006] - - [#x303C] - - [#x3041-#x3096] - - [#x309F] - - [#x30A1-#x30FA] - - [#x30FF] - - [#x3105-#x312F] - - [#x3131-#x318E] - - [#x31A0-#x31BF] - - [#x31F0-#x31FF] - - [#x4DBF] - - [#x9FFF-#xA014] - - [#xA016-#xA48C] - - [#xA4D0-#xA4F7] - - [#xA500-#xA60B] - - [#xA610-#xA61F] - - [#xA62A-#xA62B] - - [#xA66E] - - [#xA6A0-#xA6E5] - - [#xA78F] - - [#xA7F7] - - [#xA7FB-#xA801] - - [#xA803-#xA805] - - [#xA807-#xA80A] - - [#xA80C-#xA822] - - [#xA840-#xA873] - - [#xA882-#xA8B3] - - [#xA8F2-#xA8F7] - - [#xA8FB] - - [#xA8FD-#xA8FE] - - [#xA90A-#xA925] - - [#xA930-#xA946] - - [#xA960-#xA97C] - - [#xA984-#xA9B2] - - [#xA9E0-#xA9E4] - - [#xA9E7-#xA9EF] - - [#xA9FA-#xA9FE] - - [#xAA00-#xAA28] - - [#xAA40-#xAA42] - - [#xAA44-#xAA4B] - - [#xAA60-#xAA6F] - - [#xAA71-#xAA76] - - [#xAA7A] - - [#xAA7E-#xAAAF] - - [#xAAB1] - - [#xAAB5-#xAAB6] - - [#xAAB9-#xAABD] - - [#xAAC0] - - [#xAAC2] - - [#xAADB-#xAADC] - - [#xAAE0-#xAAEA] - - [#xAAF2] - - [#xAB01-#xAB06] - - [#xAB09-#xAB0E] - - [#xAB11-#xAB16] - - [#xAB20-#xAB26] - - [#xAB28-#xAB2E] - - [#xABC0-#xABE2] - - [#xD7A3] - - [#xD7B0-#xD7C6] - - [#xD7CB-#xD7FB] - - [#xF900-#xFA6D] - - [#xFA70-#xFAD9] - - [#xFB1D] - - [#xFB1F-#xFB28] - - [#xFB2A-#xFB36] - - [#xFB38-#xFB3C] - - [#xFB3E] - - [#xFB40-#xFB41] - - [#xFB43-#xFB44] - - [#xFB46-#xFBB1] - - [#xFBD3-#xFD3D] - - [#xFD50-#xFD8F] - - [#xFD92-#xFDC7] - - [#xFDF0-#xFDFB] - - [#xFE70-#xFE74] - - [#xFE76-#xFEFC] - - [#xFF66-#xFF6F] - - [#xFF71-#xFF9D] - - [#xFFA0-#xFFBE] - - [#xFFC2-#xFFC7] - - [#xFFCA-#xFFCF] - - [#xFFD2-#xFFD7] - - [#xFFDA-#xFFDC] - - -
- -
Lo       ::= [#xAA#xBA#x1BB#x1C0-#x1C3#x294#x5D0-#x5EA#x5EF-#x5F2#x620-#x63F#x641-#x64A#x66E-#x66F#x671-#x6D3#x6D5#x6EE-#x6EF#x6FA-#x6FC#x6FF#x710#x712-#x72F#x74D-#x7A5#x7B1#x7CA-#x7EA#x800-#x815#x840-#x858#x860-#x86A#x870-#x887#x889-#x88E#x8A0-#x8C8#x904-#x939#x93D#x950#x958-#x961#x972-#x980#x985-#x98C#x98F-#x990#x993-#x9A8#x9AA-#x9B0#x9B2#x9B6-#x9B9#x9BD#x9CE#x9DC-#x9DD#x9DF-#x9E1#x9F0-#x9F1#x9FC#xA05-#xA0A#xA0F-#xA10#xA13-#xA28#xA2A-#xA30#xA32-#xA33#xA35-#xA36#xA38-#xA39#xA59-#xA5C#xA5E#xA72-#xA74#xA85-#xA8D#xA8F-#xA91#xA93-#xAA8#xAAA-#xAB0#xAB2-#xAB3#xAB5-#xAB9#xABD#xAD0#xAE0-#xAE1#xAF9#xB05-#xB0C#xB0F-#xB10#xB13-#xB28#xB2A-#xB30#xB32-#xB33#xB35-#xB39#xB3D#xB5C-#xB5D#xB5F-#xB61#xB71#xB83#xB85-#xB8A#xB8E-#xB90#xB92-#xB95#xB99-#xB9A#xB9C#xB9E-#xB9F#xBA3-#xBA4#xBA8-#xBAA#xBAE-#xBB9#xBD0#xC05-#xC0C#xC0E-#xC10#xC12-#xC28#xC2A-#xC39#xC3D#xC58-#xC5A#xC5D#xC60-#xC61#xC80#xC85-#xC8C#xC8E-#xC90#xC92-#xCA8#xCAA-#xCB3#xCB5-#xCB9#xCBD#xCDD-#xCDE#xCE0-#xCE1#xCF1-#xCF2#xD04-#xD0C#xD0E-#xD10#xD12-#xD3A#xD3D#xD4E#xD54-#xD56#xD5F-#xD61#xD7A-#xD7F#xD85-#xD96#xD9A-#xDB1#xDB3-#xDBB#xDBD#xDC0-#xDC6#xE01-#xE30#xE32-#xE33#xE40-#xE45#xE81-#xE82#xE84#xE86-#xE8A#xE8C-#xEA3#xEA5#xEA7-#xEB0#xEB2-#xEB3#xEBD#xEC0-#xEC4#xEDC-#xEDF#xF00#xF40-#xF47#xF49-#xF6C#xF88-#xF8C#x1000-#x102A#x103F#x1050-#x1055#x105A-#x105D#x1061#x1065-#x1066#x106E-#x1070#x1075-#x1081#x108E#x1100-#x1248#x124A-#x124D#x1250-#x1256#x1258#x125A-#x125D#x1260-#x1288#x128A-#x128D#x1290-#x12B0#x12B2-#x12B5#x12B8-#x12BE#x12C0#x12C2-#x12C5#x12C8-#x12D6#x12D8-#x1310#x1312-#x1315#x1318-#x135A#x1380-#x138F#x1401-#x166C#x166F-#x167F#x1681-#x169A#x16A0-#x16EA#x16F1-#x16F8#x1700-#x1711#x171F-#x1731#x1740-#x1751#x1760-#x176C#x176E-#x1770#x1780-#x17B3#x17DC#x1820-#x1842#x1844-#x1878#x1880-#x1884#x1887-#x18A8#x18AA#x18B0-#x18F5#x1900-#x191E#x1950-#x196D#x1970-#x1974#x1980-#x19AB#x19B0-#x19C9#x1A00-#x1A16#x1A20-#x1A54#x1B05-#x1B33#x1B45-#x1B4C#x1B83-#x1BA0#x1BAE-#x1BAF#x1BBA-#x1BE5#x1C00-#x1C23#x1C4D-#x1C4F#x1C5A-#x1C77#x1CE9-#x1CEC#x1CEE-#x1CF3#x1CF5-#x1CF6#x1CFA#x2135-#x2138#x2D30-#x2D67#x2D80-#x2D96#x2DA0-#x2DA6#x2DA8-#x2DAE#x2DB0-#x2DB6#x2DB8-#x2DBE#x2DC0-#x2DC6#x2DC8-#x2DCE#x2DD0-#x2DD6#x2DD8-#x2DDE#x3006#x303C#x3041-#x3096#x309F#x30A1-#x30FA#x30FF#x3105-#x312F#x3131-#x318E#x31A0-#x31BF#x31F0-#x31FF#x4DBF#x9FFF-#xA014#xA016-#xA48C#xA4D0-#xA4F7#xA500-#xA60B#xA610-#xA61F#xA62A-#xA62B#xA66E#xA6A0-#xA6E5#xA78F#xA7F7#xA7FB-#xA801#xA803-#xA805#xA807-#xA80A#xA80C-#xA822#xA840-#xA873#xA882-#xA8B3#xA8F2-#xA8F7#xA8FB#xA8FD-#xA8FE#xA90A-#xA925#xA930-#xA946#xA960-#xA97C#xA984-#xA9B2#xA9E0-#xA9E4#xA9E7-#xA9EF#xA9FA-#xA9FE#xAA00-#xAA28#xAA40-#xAA42#xAA44-#xAA4B#xAA60-#xAA6F#xAA71-#xAA76#xAA7A#xAA7E-#xAAAF#xAAB1#xAAB5-#xAAB6#xAAB9-#xAABD#xAAC0#xAAC2#xAADB-#xAADC#xAAE0-#xAAEA#xAAF2#xAB01-#xAB06#xAB09-#xAB0E#xAB11-#xAB16#xAB20-#xAB26#xAB28-#xAB2E#xABC0-#xABE2#xD7A3#xD7B0-#xD7C6#xD7CB-#xD7FB#xF900-#xFA6D#xFA70-#xFAD9#xFB1D#xFB1F-#xFB28#xFB2A-#xFB36#xFB38-#xFB3C#xFB3E#xFB40-#xFB41#xFB43-#xFB44#xFB46-#xFBB1#xFBD3-#xFD3D#xFD50-#xFD8F#xFD92-#xFDC7#xFDF0-#xFDFB#xFE70-#xFE74#xFE76-#xFEFC#xFF66-#xFF6F#xFF71-#xFF9D#xFFA0-#xFFBE#xFFC2-#xFFC7#xFFCA-#xFFCF#xFFD2-#xFFD7#xFFDA-#xFFDC]
-
- Referenced by: -
- - -====================================================================================================================== -Lt -====================================================================================================================== - - -.. raw:: html - - - - - - [#x1C5] - - [#x1C8] - - [#x1CB] - - [#x1F2] - - [#x1F88-#x1F8F] - - [#x1F98-#x1F9F] - - [#x1FA8-#x1FAF] - - [#x1FBC] - - [#x1FCC] - - [#x1FFC] - - -
- -
Lt       ::= [#x1C5#x1C8#x1CB#x1F2#x1F88-#x1F8F#x1F98-#x1F9F#x1FA8-#x1FAF#x1FBC#x1FCC#x1FFC]
-
- Referenced by: -
- - -====================================================================================================================== -Lu -====================================================================================================================== - - -.. raw:: html - - - - - - [A-Z] - - [#xC0-#xD6] - - [#xD8-#xDE] - - [#x100] - - [#x102] - - [#x104] - - [#x106] - - [#x108] - - [#x10A] - - [#x10C] - - [#x10E] - - [#x110] - - [#x112] - - [#x114] - - [#x116] - - [#x118] - - [#x11A] - - [#x11C] - - [#x11E] - - [#x120] - - [#x122] - - [#x124] - - [#x126] - - [#x128] - - [#x12A] - - [#x12C] - - [#x12E] - - [#x130] - - [#x132] - - [#x134] - - [#x136] - - [#x139] - - [#x13B] - - [#x13D] - - [#x13F] - - [#x141] - - [#x143] - - [#x145] - - [#x147] - - [#x14A] - - [#x14C] - - [#x14E] - - [#x150] - - [#x152] - - [#x154] - - [#x156] - - [#x158] - - [#x15A] - - [#x15C] - - [#x15E] - - [#x160] - - [#x162] - - [#x164] - - [#x166] - - [#x168] - - [#x16A] - - [#x16C] - - [#x16E] - - [#x170] - - [#x172] - - [#x174] - - [#x176] - - [#x178-#x179] - - [#x17B] - - [#x17D] - - [#x181-#x182] - - [#x184] - - [#x186-#x187] - - [#x189-#x18B] - - [#x18E-#x191] - - [#x193-#x194] - - [#x196-#x198] - - [#x19C-#x19D] - - [#x19F-#x1A0] - - [#x1A2] - - [#x1A4] - - [#x1A6-#x1A7] - - [#x1A9] - - [#x1AC] - - [#x1AE-#x1AF] - - [#x1B1-#x1B3] - - [#x1B5] - - [#x1B7-#x1B8] - - [#x1BC] - - [#x1C4] - - [#x1C7] - - [#x1CA] - - [#x1CD] - - [#x1CF] - - [#x1D1] - - [#x1D3] - - [#x1D5] - - [#x1D7] - - [#x1D9] - - [#x1DB] - - [#x1DE] - - [#x1E0] - - [#x1E2] - - [#x1E4] - - [#x1E6] - - [#x1E8] - - [#x1EA] - - [#x1EC] - - [#x1EE] - - [#x1F1] - - [#x1F4] - - [#x1F6-#x1F8] - - [#x1FA] - - [#x1FC] - - [#x1FE] - - [#x200] - - [#x202] - - [#x204] - - [#x206] - - [#x208] - - [#x20A] - - [#x20C] - - [#x20E] - - [#x210] - - [#x212] - - [#x214] - - [#x216] - - [#x218] - - [#x21A] - - [#x21C] - - [#x21E] - - [#x220] - - [#x222] - - [#x224] - - [#x226] - - [#x228] - - [#x22A] - - [#x22C] - - [#x22E] - - [#x230] - - [#x232] - - [#x23A-#x23B] - - [#x23D-#x23E] - - [#x241] - - [#x243-#x246] - - [#x248] - - [#x24A] - - [#x24C] - - [#x24E] - - [#x370] - - [#x372] - - [#x376] - - [#x37F] - - [#x386] - - [#x388-#x38A] - - [#x38C] - - [#x38E-#x38F] - - [#x391-#x3A1] - - [#x3A3-#x3AB] - - [#x3CF] - - [#x3D2-#x3D4] - - [#x3D8] - - [#x3DA] - - [#x3DC] - - [#x3DE] - - [#x3E0] - - [#x3E2] - - [#x3E4] - - [#x3E6] - - [#x3E8] - - [#x3EA] - - [#x3EC] - - [#x3EE] - - [#x3F4] - - [#x3F7] - - [#x3F9-#x3FA] - - [#x3FD-#x42F] - - [#x460] - - [#x462] - - [#x464] - - [#x466] - - [#x468] - - [#x46A] - - [#x46C] - - [#x46E] - - [#x470] - - [#x472] - - [#x474] - - [#x476] - - [#x478] - - [#x47A] - - [#x47C] - - [#x47E] - - [#x480] - - [#x48A] - - [#x48C] - - [#x48E] - - [#x490] - - [#x492] - - [#x494] - - [#x496] - - [#x498] - - [#x49A] - - [#x49C] - - [#x49E] - - [#x4A0] - - [#x4A2] - - [#x4A4] - - [#x4A6] - - [#x4A8] - - [#x4AA] - - [#x4AC] - - [#x4AE] - - [#x4B0] - - [#x4B2] - - [#x4B4] - - [#x4B6] - - [#x4B8] - - [#x4BA] - - [#x4BC] - - [#x4BE] - - [#x4C0-#x4C1] - - [#x4C3] - - [#x4C5] - - [#x4C7] - - [#x4C9] - - [#x4CB] - - [#x4CD] - - [#x4D0] - - [#x4D2] - - [#x4D4] - - [#x4D6] - - [#x4D8] - - [#x4DA] - - [#x4DC] - - [#x4DE] - - [#x4E0] - - [#x4E2] - - [#x4E4] - - [#x4E6] - - [#x4E8] - - [#x4EA] - - [#x4EC] - - [#x4EE] - - [#x4F0] - - [#x4F2] - - [#x4F4] - - [#x4F6] - - [#x4F8] - - [#x4FA] - - [#x4FC] - - [#x4FE] - - [#x500] - - [#x502] - - [#x504] - - [#x506] - - [#x508] - - [#x50A] - - [#x50C] - - [#x50E] - - [#x510] - - [#x512] - - [#x514] - - [#x516] - - [#x518] - - [#x51A] - - [#x51C] - - [#x51E] - - [#x520] - - [#x522] - - [#x524] - - [#x526] - - [#x528] - - [#x52A] - - [#x52C] - - [#x52E] - - [#x531-#x556] - - [#x10A0-#x10C5] - - [#x10C7] - - [#x10CD] - - [#x13A0-#x13F5] - - [#x1C90-#x1CBA] - - [#x1CBD-#x1CBF] - - [#x1E00] - - [#x1E02] - - [#x1E04] - - [#x1E06] - - [#x1E08] - - [#x1E0A] - - [#x1E0C] - - [#x1E0E] - - [#x1E10] - - [#x1E12] - - [#x1E14] - - [#x1E16] - - [#x1E18] - - [#x1E1A] - - [#x1E1C] - - [#x1E1E] - - [#x1E20] - - [#x1E22] - - [#x1E24] - - [#x1E26] - - [#x1E28] - - [#x1E2A] - - [#x1E2C] - - [#x1E2E] - - [#x1E30] - - [#x1E32] - - [#x1E34] - - [#x1E36] - - [#x1E38] - - [#x1E3A] - - [#x1E3C] - - [#x1E3E] - - [#x1E40] - - [#x1E42] - - [#x1E44] - - [#x1E46] - - [#x1E48] - - [#x1E4A] - - [#x1E4C] - - [#x1E4E] - - [#x1E50] - - [#x1E52] - - [#x1E54] - - [#x1E56] - - [#x1E58] - - [#x1E5A] - - [#x1E5C] - - [#x1E5E] - - [#x1E60] - - [#x1E62] - - [#x1E64] - - [#x1E66] - - [#x1E68] - - [#x1E6A] - - [#x1E6C] - - [#x1E6E] - - [#x1E70] - - [#x1E72] - - [#x1E74] - - [#x1E76] - - [#x1E78] - - [#x1E7A] - - [#x1E7C] - - [#x1E7E] - - [#x1E80] - - [#x1E82] - - [#x1E84] - - [#x1E86] - - [#x1E88] - - [#x1E8A] - - [#x1E8C] - - [#x1E8E] - - [#x1E90] - - [#x1E92] - - [#x1E94] - - [#x1E9E] - - [#x1EA0] - - [#x1EA2] - - [#x1EA4] - - [#x1EA6] - - [#x1EA8] - - [#x1EAA] - - [#x1EAC] - - [#x1EAE] - - [#x1EB0] - - [#x1EB2] - - [#x1EB4] - - [#x1EB6] - - [#x1EB8] - - [#x1EBA] - - [#x1EBC] - - [#x1EBE] - - [#x1EC0] - - [#x1EC2] - - [#x1EC4] - - [#x1EC6] - - [#x1EC8] - - [#x1ECA] - - [#x1ECC] - - [#x1ECE] - - [#x1ED0] - - [#x1ED2] - - [#x1ED4] - - [#x1ED6] - - [#x1ED8] - - [#x1EDA] - - [#x1EDC] - - [#x1EDE] - - [#x1EE0] - - [#x1EE2] - - [#x1EE4] - - [#x1EE6] - - [#x1EE8] - - [#x1EEA] - - [#x1EEC] - - [#x1EEE] - - [#x1EF0] - - [#x1EF2] - - [#x1EF4] - - [#x1EF6] - - [#x1EF8] - - [#x1EFA] - - [#x1EFC] - - [#x1EFE] - - [#x1F08-#x1F0F] - - [#x1F18-#x1F1D] - - [#x1F28-#x1F2F] - - [#x1F38-#x1F3F] - - [#x1F48-#x1F4D] - - [#x1F59] - - [#x1F5B] - - [#x1F5D] - - [#x1F5F] - - [#x1F68-#x1F6F] - - [#x1FB8-#x1FBB] - - [#x1FC8-#x1FCB] - - [#x1FD8-#x1FDB] - - [#x1FE8-#x1FEC] - - [#x1FF8-#x1FFB] - - [#x2102] - - [#x2107] - - [#x210B-#x210D] - - [#x2110-#x2112] - - [#x2115] - - [#x2119-#x211D] - - [#x2124] - - [#x2126] - - [#x2128] - - [#x212A-#x212D] - - [#x2130-#x2133] - - [#x213E-#x213F] - - [#x2145] - - [#x2183] - - [#x2C00-#x2C2F] - - [#x2C60] - - [#x2C62-#x2C64] - - [#x2C67] - - [#x2C69] - - [#x2C6B] - - [#x2C6D-#x2C70] - - [#x2C72] - - [#x2C75] - - [#x2C7E-#x2C80] - - [#x2C82] - - [#x2C84] - - [#x2C86] - - [#x2C88] - - [#x2C8A] - - [#x2C8C] - - [#x2C8E] - - [#x2C90] - - [#x2C92] - - [#x2C94] - - [#x2C96] - - [#x2C98] - - [#x2C9A] - - [#x2C9C] - - [#x2C9E] - - [#x2CA0] - - [#x2CA2] - - [#x2CA4] - - [#x2CA6] - - [#x2CA8] - - [#x2CAA] - - [#x2CAC] - - [#x2CAE] - - [#x2CB0] - - [#x2CB2] - - [#x2CB4] - - [#x2CB6] - - [#x2CB8] - - [#x2CBA] - - [#x2CBC] - - [#x2CBE] - - [#x2CC0] - - [#x2CC2] - - [#x2CC4] - - [#x2CC6] - - [#x2CC8] - - [#x2CCA] - - [#x2CCC] - - [#x2CCE] - - [#x2CD0] - - [#x2CD2] - - [#x2CD4] - - [#x2CD6] - - [#x2CD8] - - [#x2CDA] - - [#x2CDC] - - [#x2CDE] - - [#x2CE0] - - [#x2CE2] - - [#x2CEB] - - [#x2CED] - - [#x2CF2] - - [#xA640] - - [#xA642] - - [#xA644] - - [#xA646] - - [#xA648] - - [#xA64A] - - [#xA64C] - - [#xA64E] - - [#xA650] - - [#xA652] - - [#xA654] - - [#xA656] - - [#xA658] - - [#xA65A] - - [#xA65C] - - [#xA65E] - - [#xA660] - - [#xA662] - - [#xA664] - - [#xA666] - - [#xA668] - - [#xA66A] - - [#xA66C] - - [#xA680] - - [#xA682] - - [#xA684] - - [#xA686] - - [#xA688] - - [#xA68A] - - [#xA68C] - - [#xA68E] - - [#xA690] - - [#xA692] - - [#xA694] - - [#xA696] - - [#xA698] - - [#xA69A] - - [#xA722] - - [#xA724] - - [#xA726] - - [#xA728] - - [#xA72A] - - [#xA72C] - - [#xA72E] - - [#xA732] - - [#xA734] - - [#xA736] - - [#xA738] - - [#xA73A] - - [#xA73C] - - [#xA73E] - - [#xA740] - - [#xA742] - - [#xA744] - - [#xA746] - - [#xA748] - - [#xA74A] - - [#xA74C] - - [#xA74E] - - [#xA750] - - [#xA752] - - [#xA754] - - [#xA756] - - [#xA758] - - [#xA75A] - - [#xA75C] - - [#xA75E] - - [#xA760] - - [#xA762] - - [#xA764] - - [#xA766] - - [#xA768] - - [#xA76A] - - [#xA76C] - - [#xA76E] - - [#xA779] - - [#xA77B] - - [#xA77D-#xA77E] - - [#xA780] - - [#xA782] - - [#xA784] - - [#xA786] - - [#xA78B] - - [#xA78D] - - [#xA790] - - [#xA792] - - [#xA796] - - [#xA798] - - [#xA79A] - - [#xA79C] - - [#xA79E] - - [#xA7A0] - - [#xA7A2] - - [#xA7A4] - - [#xA7A6] - - [#xA7A8] - - [#xA7AA-#xA7AE] - - [#xA7B0-#xA7B4] - - [#xA7B6] - - [#xA7B8] - - [#xA7BA] - - [#xA7BC] - - [#xA7BE] - - [#xA7C0] - - [#xA7C2] - - [#xA7C4-#xA7C7] - - [#xA7C9] - - [#xA7D0] - - [#xA7D6] - - [#xA7D8] - - [#xA7F5] - - [#xFF21-#xFF3A] - - -
- -
Lu       ::= [A-Z#xC0-#xD6#xD8-#xDE#x100#x102#x104#x106#x108#x10A#x10C#x10E#x110#x112#x114#x116#x118#x11A#x11C#x11E#x120#x122#x124#x126#x128#x12A#x12C#x12E#x130#x132#x134#x136#x139#x13B#x13D#x13F#x141#x143#x145#x147#x14A#x14C#x14E#x150#x152#x154#x156#x158#x15A#x15C#x15E#x160#x162#x164#x166#x168#x16A#x16C#x16E#x170#x172#x174#x176#x178-#x179#x17B#x17D#x181-#x182#x184#x186-#x187#x189-#x18B#x18E-#x191#x193-#x194#x196-#x198#x19C-#x19D#x19F-#x1A0#x1A2#x1A4#x1A6-#x1A7#x1A9#x1AC#x1AE-#x1AF#x1B1-#x1B3#x1B5#x1B7-#x1B8#x1BC#x1C4#x1C7#x1CA#x1CD#x1CF#x1D1#x1D3#x1D5#x1D7#x1D9#x1DB#x1DE#x1E0#x1E2#x1E4#x1E6#x1E8#x1EA#x1EC#x1EE#x1F1#x1F4#x1F6-#x1F8#x1FA#x1FC#x1FE#x200#x202#x204#x206#x208#x20A#x20C#x20E#x210#x212#x214#x216#x218#x21A#x21C#x21E#x220#x222#x224#x226#x228#x22A#x22C#x22E#x230#x232#x23A-#x23B#x23D-#x23E#x241#x243-#x246#x248#x24A#x24C#x24E#x370#x372#x376#x37F#x386#x388-#x38A#x38C#x38E-#x38F#x391-#x3A1#x3A3-#x3AB#x3CF#x3D2-#x3D4#x3D8#x3DA#x3DC#x3DE#x3E0#x3E2#x3E4#x3E6#x3E8#x3EA#x3EC#x3EE#x3F4#x3F7#x3F9-#x3FA#x3FD-#x42F#x460#x462#x464#x466#x468#x46A#x46C#x46E#x470#x472#x474#x476#x478#x47A#x47C#x47E#x480#x48A#x48C#x48E#x490#x492#x494#x496#x498#x49A#x49C#x49E#x4A0#x4A2#x4A4#x4A6#x4A8#x4AA#x4AC#x4AE#x4B0#x4B2#x4B4#x4B6#x4B8#x4BA#x4BC#x4BE#x4C0-#x4C1#x4C3#x4C5#x4C7#x4C9#x4CB#x4CD#x4D0#x4D2#x4D4#x4D6#x4D8#x4DA#x4DC#x4DE#x4E0#x4E2#x4E4#x4E6#x4E8#x4EA#x4EC#x4EE#x4F0#x4F2#x4F4#x4F6#x4F8#x4FA#x4FC#x4FE#x500#x502#x504#x506#x508#x50A#x50C#x50E#x510#x512#x514#x516#x518#x51A#x51C#x51E#x520#x522#x524#x526#x528#x52A#x52C#x52E#x531-#x556#x10A0-#x10C5#x10C7#x10CD#x13A0-#x13F5#x1C90-#x1CBA#x1CBD-#x1CBF#x1E00#x1E02#x1E04#x1E06#x1E08#x1E0A#x1E0C#x1E0E#x1E10#x1E12#x1E14#x1E16#x1E18#x1E1A#x1E1C#x1E1E#x1E20#x1E22#x1E24#x1E26#x1E28#x1E2A#x1E2C#x1E2E#x1E30#x1E32#x1E34#x1E36#x1E38#x1E3A#x1E3C#x1E3E#x1E40#x1E42#x1E44#x1E46#x1E48#x1E4A#x1E4C#x1E4E#x1E50#x1E52#x1E54#x1E56#x1E58#x1E5A#x1E5C#x1E5E#x1E60#x1E62#x1E64#x1E66#x1E68#x1E6A#x1E6C#x1E6E#x1E70#x1E72#x1E74#x1E76#x1E78#x1E7A#x1E7C#x1E7E#x1E80#x1E82#x1E84#x1E86#x1E88#x1E8A#x1E8C#x1E8E#x1E90#x1E92#x1E94#x1E9E#x1EA0#x1EA2#x1EA4#x1EA6#x1EA8#x1EAA#x1EAC#x1EAE#x1EB0#x1EB2#x1EB4#x1EB6#x1EB8#x1EBA#x1EBC#x1EBE#x1EC0#x1EC2#x1EC4#x1EC6#x1EC8#x1ECA#x1ECC#x1ECE#x1ED0#x1ED2#x1ED4#x1ED6#x1ED8#x1EDA#x1EDC#x1EDE#x1EE0#x1EE2#x1EE4#x1EE6#x1EE8#x1EEA#x1EEC#x1EEE#x1EF0#x1EF2#x1EF4#x1EF6#x1EF8#x1EFA#x1EFC#x1EFE#x1F08-#x1F0F#x1F18-#x1F1D#x1F28-#x1F2F#x1F38-#x1F3F#x1F48-#x1F4D#x1F59#x1F5B#x1F5D#x1F5F#x1F68-#x1F6F#x1FB8-#x1FBB#x1FC8-#x1FCB#x1FD8-#x1FDB#x1FE8-#x1FEC#x1FF8-#x1FFB#x2102#x2107#x210B-#x210D#x2110-#x2112#x2115#x2119-#x211D#x2124#x2126#x2128#x212A-#x212D#x2130-#x2133#x213E-#x213F#x2145#x2183#x2C00-#x2C2F#x2C60#x2C62-#x2C64#x2C67#x2C69#x2C6B#x2C6D-#x2C70#x2C72#x2C75#x2C7E-#x2C80#x2C82#x2C84#x2C86#x2C88#x2C8A#x2C8C#x2C8E#x2C90#x2C92#x2C94#x2C96#x2C98#x2C9A#x2C9C#x2C9E#x2CA0#x2CA2#x2CA4#x2CA6#x2CA8#x2CAA#x2CAC#x2CAE#x2CB0#x2CB2#x2CB4#x2CB6#x2CB8#x2CBA#x2CBC#x2CBE#x2CC0#x2CC2#x2CC4#x2CC6#x2CC8#x2CCA#x2CCC#x2CCE#x2CD0#x2CD2#x2CD4#x2CD6#x2CD8#x2CDA#x2CDC#x2CDE#x2CE0#x2CE2#x2CEB#x2CED#x2CF2#xA640#xA642#xA644#xA646#xA648#xA64A#xA64C#xA64E#xA650#xA652#xA654#xA656#xA658#xA65A#xA65C#xA65E#xA660#xA662#xA664#xA666#xA668#xA66A#xA66C#xA680#xA682#xA684#xA686#xA688#xA68A#xA68C#xA68E#xA690#xA692#xA694#xA696#xA698#xA69A#xA722#xA724#xA726#xA728#xA72A#xA72C#xA72E#xA732#xA734#xA736#xA738#xA73A#xA73C#xA73E#xA740#xA742#xA744#xA746#xA748#xA74A#xA74C#xA74E#xA750#xA752#xA754#xA756#xA758#xA75A#xA75C#xA75E#xA760#xA762#xA764#xA766#xA768#xA76A#xA76C#xA76E#xA779#xA77B#xA77D-#xA77E#xA780#xA782#xA784#xA786#xA78B#xA78D#xA790#xA792#xA796#xA798#xA79A#xA79C#xA79E#xA7A0#xA7A2#xA7A4#xA7A6#xA7A8#xA7AA-#xA7AE#xA7B0-#xA7B4#xA7B6#xA7B8#xA7BA#xA7BC#xA7BE#xA7C0#xA7C2#xA7C4-#xA7C7#xA7C9#xA7D0#xA7D6#xA7D8#xA7F5#xFF21-#xFF3A]
-
- Referenced by: -
- - -====================================================================================================================== -Nl -====================================================================================================================== - - -.. raw:: html - - - - - - [#x16EE-#x16F0] - - [#x2160-#x2182] - - [#x2185-#x2188] - - [#x3007] - - [#x3021-#x3029] - - [#x3038-#x303A] - - [#xA6E6-#xA6EF] - - -
- -
Nl       ::= [#x16EE-#x16F0#x2160-#x2182#x2185-#x2188#x3007#x3021-#x3029#x3038-#x303A#xA6E6-#xA6EF]
-
- Referenced by: -
- - -====================================================================================================================== -UnicodeIdentifierExtend -====================================================================================================================== - - -.. raw:: html - - - - - - Mn - - Mc - - Nd - - Pc - - Cf - - CJK - -
- - -
         ::= Mn
-
           | Mc
-
           | Nd
-
           | Pc
-
           | Cf
-
           | CJK
-
- Referenced by: -
- - -====================================================================================================================== -Cf -====================================================================================================================== - - -.. raw:: html - - - - - - [#xAD] - - [#x600-#x605] - - [#x61C] - - [#x6DD] - - [#x70F] - - [#x890-#x891] - - [#x8E2] - - [#x180E] - - [#x200B-#x200F] - - [#x202A-#x202E] - - [#x2060-#x2064] - - [#x2066-#x206F] - - [#xFEFF] - - [#xFFF9-#xFFFB] - - -
- -
Cf       ::= [#xAD#x600-#x605#x61C#x6DD#x70F#x890-#x891#x8E2#x180E#x200B-#x200F#x202A-#x202E#x2060-#x2064#x2066-#x206F#xFEFF#xFFF9-#xFFFB]
-
- Referenced by: -
- - -====================================================================================================================== -Mc -====================================================================================================================== - - -.. raw:: html - - - - - - [#x903] - - [#x93B] - - [#x93E-#x940] - - [#x949-#x94C] - - [#x94E-#x94F] - - [#x982-#x983] - - [#x9BE-#x9C0] - - [#x9C7-#x9C8] - - [#x9CB-#x9CC] - - [#x9D7] - - [#xA03] - - [#xA3E-#xA40] - - [#xA83] - - [#xABE-#xAC0] - - [#xAC9] - - [#xACB-#xACC] - - [#xB02-#xB03] - - [#xB3E] - - [#xB40] - - [#xB47-#xB48] - - [#xB4B-#xB4C] - - [#xB57] - - [#xBBE-#xBBF] - - [#xBC1-#xBC2] - - [#xBC6-#xBC8] - - [#xBCA-#xBCC] - - [#xBD7] - - [#xC01-#xC03] - - [#xC41-#xC44] - - [#xC82-#xC83] - - [#xCBE] - - [#xCC0-#xCC4] - - [#xCC7-#xCC8] - - [#xCCA-#xCCB] - - [#xCD5-#xCD6] - - [#xCF3] - - [#xD02-#xD03] - - [#xD3E-#xD40] - - [#xD46-#xD48] - - [#xD4A-#xD4C] - - [#xD57] - - [#xD82-#xD83] - - [#xDCF-#xDD1] - - [#xDD8-#xDDF] - - [#xDF2-#xDF3] - - [#xF3E-#xF3F] - - [#xF7F] - - [#x102B-#x102C] - - [#x1031] - - [#x1038] - - [#x103B-#x103C] - - [#x1056-#x1057] - - [#x1062-#x1064] - - [#x1067-#x106D] - - [#x1083-#x1084] - - [#x1087-#x108C] - - [#x108F] - - [#x109A-#x109C] - - [#x1715] - - [#x1734] - - [#x17B6] - - [#x17BE-#x17C5] - - [#x17C7-#x17C8] - - [#x1923-#x1926] - - [#x1929-#x192B] - - [#x1930-#x1931] - - [#x1933-#x1938] - - [#x1A19-#x1A1A] - - [#x1A55] - - [#x1A57] - - [#x1A61] - - [#x1A63-#x1A64] - - [#x1A6D-#x1A72] - - [#x1B04] - - [#x1B35] - - [#x1B3B] - - [#x1B3D-#x1B41] - - [#x1B43-#x1B44] - - [#x1B82] - - [#x1BA1] - - [#x1BA6-#x1BA7] - - [#x1BAA] - - [#x1BE7] - - [#x1BEA-#x1BEC] - - [#x1BEE] - - [#x1BF2-#x1BF3] - - [#x1C24-#x1C2B] - - [#x1C34-#x1C35] - - [#x1CE1] - - [#x1CF7] - - [#x302E-#x302F] - - [#xA823-#xA824] - - [#xA827] - - [#xA880-#xA881] - - [#xA8B4-#xA8C3] - - [#xA952-#xA953] - - [#xA983] - - [#xA9B4-#xA9B5] - - [#xA9BA-#xA9BB] - - [#xA9BE-#xA9C0] - - [#xAA2F-#xAA30] - - [#xAA33-#xAA34] - - [#xAA4D] - - [#xAA7B] - - [#xAA7D] - - [#xAAEB] - - [#xAAEE-#xAAEF] - - [#xAAF5] - - [#xABE3-#xABE4] - - [#xABE6-#xABE7] - - [#xABE9-#xABEA] - - [#xABEC] - - -
- -
Mc       ::= [#x903#x93B#x93E-#x940#x949-#x94C#x94E-#x94F#x982-#x983#x9BE-#x9C0#x9C7-#x9C8#x9CB-#x9CC#x9D7#xA03#xA3E-#xA40#xA83#xABE-#xAC0#xAC9#xACB-#xACC#xB02-#xB03#xB3E#xB40#xB47-#xB48#xB4B-#xB4C#xB57#xBBE-#xBBF#xBC1-#xBC2#xBC6-#xBC8#xBCA-#xBCC#xBD7#xC01-#xC03#xC41-#xC44#xC82-#xC83#xCBE#xCC0-#xCC4#xCC7-#xCC8#xCCA-#xCCB#xCD5-#xCD6#xCF3#xD02-#xD03#xD3E-#xD40#xD46-#xD48#xD4A-#xD4C#xD57#xD82-#xD83#xDCF-#xDD1#xDD8-#xDDF#xDF2-#xDF3#xF3E-#xF3F#xF7F#x102B-#x102C#x1031#x1038#x103B-#x103C#x1056-#x1057#x1062-#x1064#x1067-#x106D#x1083-#x1084#x1087-#x108C#x108F#x109A-#x109C#x1715#x1734#x17B6#x17BE-#x17C5#x17C7-#x17C8#x1923-#x1926#x1929-#x192B#x1930-#x1931#x1933-#x1938#x1A19-#x1A1A#x1A55#x1A57#x1A61#x1A63-#x1A64#x1A6D-#x1A72#x1B04#x1B35#x1B3B#x1B3D-#x1B41#x1B43-#x1B44#x1B82#x1BA1#x1BA6-#x1BA7#x1BAA#x1BE7#x1BEA-#x1BEC#x1BEE#x1BF2-#x1BF3#x1C24-#x1C2B#x1C34-#x1C35#x1CE1#x1CF7#x302E-#x302F#xA823-#xA824#xA827#xA880-#xA881#xA8B4-#xA8C3#xA952-#xA953#xA983#xA9B4-#xA9B5#xA9BA-#xA9BB#xA9BE-#xA9C0#xAA2F-#xAA30#xAA33-#xAA34#xAA4D#xAA7B#xAA7D#xAAEB#xAAEE-#xAAEF#xAAF5#xABE3-#xABE4#xABE6-#xABE7#xABE9-#xABEA#xABEC]
-
- Referenced by: -
- - -====================================================================================================================== -Mn -====================================================================================================================== - - -.. raw:: html - - - - - - [#x300-#x36F] - - [#x483-#x487] - - [#x591-#x5BD] - - [#x5BF] - - [#x5C1-#x5C2] - - [#x5C4-#x5C5] - - [#x5C7] - - [#x610-#x61A] - - [#x64B-#x65F] - - [#x670] - - [#x6D6-#x6DC] - - [#x6DF-#x6E4] - - [#x6E7-#x6E8] - - [#x6EA-#x6ED] - - [#x711] - - [#x730-#x74A] - - [#x7A6-#x7B0] - - [#x7EB-#x7F3] - - [#x7FD] - - [#x816-#x819] - - [#x81B-#x823] - - [#x825-#x827] - - [#x829-#x82D] - - [#x859-#x85B] - - [#x898-#x89F] - - [#x8CA-#x8E1] - - [#x8E3-#x902] - - [#x93A] - - [#x93C] - - [#x941-#x948] - - [#x94D] - - [#x951-#x957] - - [#x962-#x963] - - [#x981] - - [#x9BC] - - [#x9C1-#x9C4] - - [#x9CD] - - [#x9E2-#x9E3] - - [#x9FE] - - [#xA01-#xA02] - - [#xA3C] - - [#xA41-#xA42] - - [#xA47-#xA48] - - [#xA4B-#xA4D] - - [#xA51] - - [#xA70-#xA71] - - [#xA75] - - [#xA81-#xA82] - - [#xABC] - - [#xAC1-#xAC5] - - [#xAC7-#xAC8] - - [#xACD] - - [#xAE2-#xAE3] - - [#xAFA-#xAFF] - - [#xB01] - - [#xB3C] - - [#xB3F] - - [#xB41-#xB44] - - [#xB4D] - - [#xB55-#xB56] - - [#xB62-#xB63] - - [#xB82] - - [#xBC0] - - [#xBCD] - - [#xC00] - - [#xC04] - - [#xC3C] - - [#xC3E-#xC40] - - [#xC46-#xC48] - - [#xC4A-#xC4D] - - [#xC55-#xC56] - - [#xC62-#xC63] - - [#xC81] - - [#xCBC] - - [#xCBF] - - [#xCC6] - - [#xCCC-#xCCD] - - [#xCE2-#xCE3] - - [#xD00-#xD01] - - [#xD3B-#xD3C] - - [#xD41-#xD44] - - [#xD4D] - - [#xD62-#xD63] - - [#xD81] - - [#xDCA] - - [#xDD2-#xDD4] - - [#xDD6] - - [#xE31] - - [#xE34-#xE3A] - - [#xE47-#xE4E] - - [#xEB1] - - [#xEB4-#xEBC] - - [#xEC8-#xECE] - - [#xF18-#xF19] - - [#xF35] - - [#xF37] - - [#xF39] - - [#xF71-#xF7E] - - [#xF80-#xF84] - - [#xF86-#xF87] - - [#xF8D-#xF97] - - [#xF99-#xFBC] - - [#xFC6] - - [#x102D-#x1030] - - [#x1032-#x1037] - - [#x1039-#x103A] - - [#x103D-#x103E] - - [#x1058-#x1059] - - [#x105E-#x1060] - - [#x1071-#x1074] - - [#x1082] - - [#x1085-#x1086] - - [#x108D] - - [#x109D] - - [#x135D-#x135F] - - [#x1712-#x1714] - - [#x1732-#x1733] - - [#x1752-#x1753] - - [#x1772-#x1773] - - [#x17B4-#x17B5] - - [#x17B7-#x17BD] - - [#x17C6] - - [#x17C9-#x17D3] - - [#x17DD] - - [#x180B-#x180D] - - [#x180F] - - [#x1885-#x1886] - - [#x18A9] - - [#x1920-#x1922] - - [#x1927-#x1928] - - [#x1932] - - [#x1939-#x193B] - - [#x1A17-#x1A18] - - [#x1A1B] - - [#x1A56] - - [#x1A58-#x1A5E] - - [#x1A60] - - [#x1A62] - - [#x1A65-#x1A6C] - - [#x1A73-#x1A7C] - - [#x1A7F] - - [#x1AB0-#x1ABD] - - [#x1ABF-#x1ACE] - - [#x1B00-#x1B03] - - [#x1B34] - - [#x1B36-#x1B3A] - - [#x1B3C] - - [#x1B42] - - [#x1B6B-#x1B73] - - [#x1B80-#x1B81] - - [#x1BA2-#x1BA5] - - [#x1BA8-#x1BA9] - - [#x1BAB-#x1BAD] - - [#x1BE6] - - [#x1BE8-#x1BE9] - - [#x1BED] - - [#x1BEF-#x1BF1] - - [#x1C2C-#x1C33] - - [#x1C36-#x1C37] - - [#x1CD0-#x1CD2] - - [#x1CD4-#x1CE0] - - [#x1CE2-#x1CE8] - - [#x1CED] - - [#x1CF4] - - [#x1CF8-#x1CF9] - - [#x1DC0-#x1DFF] - - [#x20D0-#x20DC] - - [#x20E1] - - [#x20E5-#x20F0] - - [#x2CEF-#x2CF1] - - [#x2D7F] - - [#x2DE0-#x2DFF] - - [#x302A-#x302D] - - [#x3099-#x309A] - - [#xA66F] - - [#xA674-#xA67D] - - [#xA69E-#xA69F] - - [#xA6F0-#xA6F1] - - [#xA802] - - [#xA806] - - [#xA80B] - - [#xA825-#xA826] - - [#xA82C] - - [#xA8C4-#xA8C5] - - [#xA8E0-#xA8F1] - - [#xA8FF] - - [#xA926-#xA92D] - - [#xA947-#xA951] - - [#xA980-#xA982] - - [#xA9B3] - - [#xA9B6-#xA9B9] - - [#xA9BC-#xA9BD] - - [#xA9E5] - - [#xAA29-#xAA2E] - - [#xAA31-#xAA32] - - [#xAA35-#xAA36] - - [#xAA43] - - [#xAA4C] - - [#xAA7C] - - [#xAAB0] - - [#xAAB2-#xAAB4] - - [#xAAB7-#xAAB8] - - [#xAABE-#xAABF] - - [#xAAC1] - - [#xAAEC-#xAAED] - - [#xAAF6] - - [#xABE5] - - [#xABE8] - - [#xABED] - - [#xFB1E] - - [#xFE00-#xFE0F] - - [#xFE20-#xFE2F] - - -
- -
Mn       ::= [#x300-#x36F#x483-#x487#x591-#x5BD#x5BF#x5C1-#x5C2#x5C4-#x5C5#x5C7#x610-#x61A#x64B-#x65F#x670#x6D6-#x6DC#x6DF-#x6E4#x6E7-#x6E8#x6EA-#x6ED#x711#x730-#x74A#x7A6-#x7B0#x7EB-#x7F3#x7FD#x816-#x819#x81B-#x823#x825-#x827#x829-#x82D#x859-#x85B#x898-#x89F#x8CA-#x8E1#x8E3-#x902#x93A#x93C#x941-#x948#x94D#x951-#x957#x962-#x963#x981#x9BC#x9C1-#x9C4#x9CD#x9E2-#x9E3#x9FE#xA01-#xA02#xA3C#xA41-#xA42#xA47-#xA48#xA4B-#xA4D#xA51#xA70-#xA71#xA75#xA81-#xA82#xABC#xAC1-#xAC5#xAC7-#xAC8#xACD#xAE2-#xAE3#xAFA-#xAFF#xB01#xB3C#xB3F#xB41-#xB44#xB4D#xB55-#xB56#xB62-#xB63#xB82#xBC0#xBCD#xC00#xC04#xC3C#xC3E-#xC40#xC46-#xC48#xC4A-#xC4D#xC55-#xC56#xC62-#xC63#xC81#xCBC#xCBF#xCC6#xCCC-#xCCD#xCE2-#xCE3#xD00-#xD01#xD3B-#xD3C#xD41-#xD44#xD4D#xD62-#xD63#xD81#xDCA#xDD2-#xDD4#xDD6#xE31#xE34-#xE3A#xE47-#xE4E#xEB1#xEB4-#xEBC#xEC8-#xECE#xF18-#xF19#xF35#xF37#xF39#xF71-#xF7E#xF80-#xF84#xF86-#xF87#xF8D-#xF97#xF99-#xFBC#xFC6#x102D-#x1030#x1032-#x1037#x1039-#x103A#x103D-#x103E#x1058-#x1059#x105E-#x1060#x1071-#x1074#x1082#x1085-#x1086#x108D#x109D#x135D-#x135F#x1712-#x1714#x1732-#x1733#x1752-#x1753#x1772-#x1773#x17B4-#x17B5#x17B7-#x17BD#x17C6#x17C9-#x17D3#x17DD#x180B-#x180D#x180F#x1885-#x1886#x18A9#x1920-#x1922#x1927-#x1928#x1932#x1939-#x193B#x1A17-#x1A18#x1A1B#x1A56#x1A58-#x1A5E#x1A60#x1A62#x1A65-#x1A6C#x1A73-#x1A7C#x1A7F#x1AB0-#x1ABD#x1ABF-#x1ACE#x1B00-#x1B03#x1B34#x1B36-#x1B3A#x1B3C#x1B42#x1B6B-#x1B73#x1B80-#x1B81#x1BA2-#x1BA5#x1BA8-#x1BA9#x1BAB-#x1BAD#x1BE6#x1BE8-#x1BE9#x1BED#x1BEF-#x1BF1#x1C2C-#x1C33#x1C36-#x1C37#x1CD0-#x1CD2#x1CD4-#x1CE0#x1CE2-#x1CE8#x1CED#x1CF4#x1CF8-#x1CF9#x1DC0-#x1DFF#x20D0-#x20DC#x20E1#x20E5-#x20F0#x2CEF-#x2CF1#x2D7F#x2DE0-#x2DFF#x302A-#x302D#x3099-#x309A#xA66F#xA674-#xA67D#xA69E-#xA69F#xA6F0-#xA6F1#xA802#xA806#xA80B#xA825-#xA826#xA82C#xA8C4-#xA8C5#xA8E0-#xA8F1#xA8FF#xA926-#xA92D#xA947-#xA951#xA980-#xA982#xA9B3#xA9B6-#xA9B9#xA9BC-#xA9BD#xA9E5#xAA29-#xAA2E#xAA31-#xAA32#xAA35-#xAA36#xAA43#xAA4C#xAA7C#xAAB0#xAAB2-#xAAB4#xAAB7-#xAAB8#xAABE-#xAABF#xAAC1#xAAEC-#xAAED#xAAF6#xABE5#xABE8#xABED#xFB1E#xFE00-#xFE0F#xFE20-#xFE2F]
-
- Referenced by: -
- - -====================================================================================================================== -Nd -====================================================================================================================== - - -.. raw:: html - - - - - - [0-9] - - [#x660-#x669] - - [#x6F0-#x6F9] - - [#x7C0-#x7C9] - - [#x966-#x96F] - - [#x9E6-#x9EF] - - [#xA66-#xA6F] - - [#xAE6-#xAEF] - - [#xB66-#xB6F] - - [#xBE6-#xBEF] - - [#xC66-#xC6F] - - [#xCE6-#xCEF] - - [#xD66-#xD6F] - - [#xDE6-#xDEF] - - [#xE50-#xE59] - - [#xED0-#xED9] - - [#xF20-#xF29] - - [#x1040-#x1049] - - [#x1090-#x1099] - - [#x17E0-#x17E9] - - [#x1810-#x1819] - - [#x1946-#x194F] - - [#x19D0-#x19D9] - - [#x1A80-#x1A89] - - [#x1A90-#x1A99] - - [#x1B50-#x1B59] - - [#x1BB0-#x1BB9] - - [#x1C40-#x1C49] - - [#x1C50-#x1C59] - - [#xA620-#xA629] - - [#xA8D0-#xA8D9] - - [#xA900-#xA909] - - [#xA9D0-#xA9D9] - - [#xA9F0-#xA9F9] - - [#xAA50-#xAA59] - - [#xABF0-#xABF9] - - [#xFF10-#xFF19] - - -
- -
Nd       ::= [0-9#x660-#x669#x6F0-#x6F9#x7C0-#x7C9#x966-#x96F#x9E6-#x9EF#xA66-#xA6F#xAE6-#xAEF#xB66-#xB6F#xBE6-#xBEF#xC66-#xC6F#xCE6-#xCEF#xD66-#xD6F#xDE6-#xDEF#xE50-#xE59#xED0-#xED9#xF20-#xF29#x1040-#x1049#x1090-#x1099#x17E0-#x17E9#x1810-#x1819#x1946-#x194F#x19D0-#x19D9#x1A80-#x1A89#x1A90-#x1A99#x1B50-#x1B59#x1BB0-#x1BB9#x1C40-#x1C49#x1C50-#x1C59#xA620-#xA629#xA8D0-#xA8D9#xA900-#xA909#xA9D0-#xA9D9#xA9F0-#xA9F9#xAA50-#xAA59#xABF0-#xABF9#xFF10-#xFF19]
-
- Referenced by: -
- - -====================================================================================================================== -Pc -====================================================================================================================== - - -.. raw:: html - - - - - - [#x203F-#x2040] - - [#x2054] - - [#xFE33-#xFE34] - - [#xFE4D-#xFE4F] - - [#xFF3F] - - -
- -
Pc       ::= [#x203F-#x2040#x2054#xFE33-#xFE34#xFE4D-#xFE4F#xFF3F]
-
- Referenced by: -
- - -====================================================================================================================== -CJK -====================================================================================================================== - - -.. raw:: html - - - - - - [#xAC00-#xD7A3] - - [#x4E00-#x9FFF] - - -
- -
CJK      ::= [#xAC00-#xD7A3#x4E00-#x9FFF]
-
- - -====================================================================================================================== -ESC -====================================================================================================================== - - -.. raw:: html - - - - - - \ - - n - - t - - b - - r - - f - - \ - - " - - -
- -
ESC      ::= '\' [ntbrf\"]
-
- Referenced by: -
- - -====================================================================================================================== -S_CHAR_LITERAL -====================================================================================================================== - - -.. raw:: html - - - - - - U - - E - - N - - R - - B - - RB - - _utf8 - - q'{ - - . - - }' - - ' - - ESC - \' - - [^'\] - - '' - - [^'] - - ' - - $$ - - [^$] - - $$ - - q'( - - . - - )' - - q'[ - - . - - ]' - - q'' - - . - - '' - - -
- - -
         ::= ( [UENRB] | 'RB' | '_utf8' )? ( "'" ( ( ESC | "\'" | [^'\] )* | ( "''" | [^'] )+ ) "'" | '$$' [^$]* '$$' | "q'{" .* "}'" | "q'(" .* ")'" | "q'[" .* "]'" | "q''" .* "''" )
-
- - -====================================================================================================================== -S_QUOTED_IDENTIFIER -====================================================================================================================== - - -.. raw:: html - - - - - - " - - "" - - [^"#xA#xD] - - " - - ` - - [^`#xA#xD] - - ` - - [ - - [^#x5D#xA#xD] - - ] - - -
- - -
         ::= '"' ( '""' | [^"#xA#xD] )* '"'
-
           | '`' [^`#xA#xD]+ '`'
-
           | '[' [^#x5D#xA#xD]* ']'
-
- - -====================================================================================================================== -EOF -====================================================================================================================== - - -.. raw:: html - - - - - - $ - - -
- -
EOF      ::= $
-
- Referenced by: -
- - \ No newline at end of file diff --git a/src/site/sphinx/syntax_stable.rst b/src/site/sphinx/syntax_stable.rst deleted file mode 100644 index 05384cba76..0000000000 --- a/src/site/sphinx/syntax_stable.rst +++ /dev/null @@ -1,21103 +0,0 @@ - -********************************************************************* -SQL Syntax |JSQLPARSER_VERSION| -********************************************************************* - -The EBNF and Railroad Diagrams for |JSQLPARSER_VERSION|. - - -====================================================================================================================== -NonReservedWord -====================================================================================================================== - - -.. raw:: html - - - - - - ACTION - - ACTIVE - - ADD - - ADVANCE - - ADVISE - - AGAINST - - AGGREGATE - - ALGORITHM - - ALIGN - - ALTER - - ALWAYS - - ANALYZE - - APPEND_ONLY - - APPLY - - APPROXIMATE - - ARCHIVE - - ARRAY - - ASYMMETRIC - - AT - - ASC - - AUTHORIZATION - - AUTO - - AUTO_INCREMENT - - AZURE - - BASE64 - - BEFORE - - BEGIN - - BERNOULLI - - BINARY - - BIT - - BLOBSTORAGE - - BLOCK - - BOOLEAN - - BREADTH - - BRANCH - - BROWSE - - BY - - BYTES - - CACHE - - BUFFERS - - BYTE - - CALL - - CASCADE - - CASE - - CAST - - CERTIFICATE - - CHARACTER - - CHANGE - - CHANGES - - CHECKPOINT - - CHAR - - CLOSE - - CLOUD - - COALESCE - - COLLATE - - COLUMN - - COLUMNS - - COMMIT - - COMMENT - - COMMENTS - - CONFLICT - - CONSTRAINTS - - CONVERT - - CORRESPONDING - - COSTS - - COUNT - - CREATED - - CYCLE - - DATABASE - - DATA - - DECLARE - - DBA_RECYCLEBIN - - DEFAULTS - - DEPTH - - DEFERRABLE - - DELAYED - - DELETE - - DELIMIT - - DELIMITER - - DESC - - DESCRIBE - - DISABLE - - DISCARD - - DISCONNECT - - DIV - - DDL - - DML - - DO - - DOMAIN - - DRIVER - - DROP - - DUMP - - DUMPFILE - - DUPLICATE - - ELEMENTS - - ENCLOSED - - EMIT - - EMPTY - - ENABLE - - ENCODING - - ENCRYPTION - - END - - ESCAPED - - ENFORCED - - ENGINE - - ERROR - - ESCAPE - - EXA - - EXCHANGE - - EXCLUDE - - EXCLUDING - - EXCLUSIVE - - EXEC - - EXECUTE - - EXPLAIN - - EXPLICIT - - EXTENDED - - EXTRACT - - EXPORT - - K_ISOLATION - FILTER - - FIELDS - - FIRST - - FLUSH - - FOLLOWING - - FORMAT - - FULLTEXT - - FUNCTION - - GRANT - - GROUP_CONCAT - - GUARD - - HASH - - HIGH - - HIGH_PRIORITY - - HISTORY - - HOPPING - - IDENTIFIED - - IDENTITY - - INCLUDE - - INCLUDE_NULL_VALUES - - INCLUDING - - INCREMENT - - INDEX - - INFORMATION - - INSERT - - INTERLEAVE - - INTERPRET - - INVALIDATE - - INVERSE - - INVISIBLE - - ISNULL - - JDBC - - JSON - - JSON_OBJECT - - JSON_OBJECTAGG - - JSON_ARRAY - - JSON_ARRAYAGG - - KEEP - - KEY_BLOCK_SIZE - - KEY - - KEYS - - KILL - - FN - - LAST - - LEADING - - LESS - - LEVEL - - LINES - - LOCAL - - LOCK - - LOCKED - - LINK - - LOG - - LOOP - - LOW - - LOW_PRIORITY - - LTRIM - - MATCH - - MATCH_ANY - - MATCH_ALL - - MATCH_PHRASE - - MATCH_PHRASE_PREFIX - - MATCH_REGEXP - - MATCHED - - MATERIALIZED - - MAX - - MAXVALUE - - MEMBER - - MERGE - - MIN - - MINVALUE - - MODE - - MODIFY - - MOVEMENT - - NAMES - - NAME - - NEVER - - NEXT - - K_NEXTVAL - NO - - NOCACHE - - NOKEEP - - NOLOCK - - NOMAXVALUE - - NOMINVALUE - - NONE - - NOORDER - - NOTHING - - NOTNULL - - NOVALIDATE - - NULLS - - NOWAIT - - OF - - OFF - - OPTIONALLY - - OPEN - - ORA - - ORDINALITY - - OUTFILE - - OVER - - OVERFLOW - - OVERLAPS - - OVERRIDING - - OVERWRITE - - PADDING - - PARALLEL - - PARENT - - PARSER - - PARTITION - - PARTITIONING - - PATH - - PERCENT - - PLACING - - PLAN - - PLUS - - PRECEDING - - PRIMARY - - POLICY - - PURGE - - QUERY - - QUICK - - QUIESCE - - RANGE - - RAW - - READ - - REBUILD - - RECYCLEBIN - - RECURSIVE - - REFERENCES - - REFRESH - - REGEXP - - REJECT - - RESPECT - - RLIKE - - REGEXP_LIKE - - REGISTER - - REMOTE - - REMOVE - - RENAME - - REORGANIZE - - REPAIR - - REPEATABLE - - REPLACE - - RESET - - RESTART - - RESUMABLE - - RESUME - - RESTRICT - - RESTRICTED - - RETURN - - ROLLBACK - - ROLLUP - - ROOT - - ROW - - ROWS - - RTRIM - - SAFE_CAST - - SAFE_CONVERT - - SAVEPOINT - - SCHEMA - - SEARCH - - SECURE - - SECURITY - - SEED - - SEQUENCE - - SEPARATOR - - SESSION - - SETS - - SHOW - - SHUTDOWN - - SHARE - - SIBLINGS - - SIMILAR - - SIZE - - SKIP - - SPATIAL - - STARTING - - STORED - - STREAM - - STRICT - - STRING - - STRUCT - - SUMMARIZE - - SUSPEND - - SWITCH - - SYMMETRIC - - SYNONYM - - SYSTEM - - SYSTEM_TIME - - SYSTEM_TIMESTAMP - - SYSTEM_VERSION - - TABLE - - TABLESPACE - - TERMINATED - - TRIGGER - - THEN - - TEMP - - K_TEXT_LITERAL - TEMPORARY - - THAN - - K_TIME_KEY_EXPR - TIMEOUT - - TO - - TRIM - - TRUNCATE - - TRY_CAST - - TRY_CONVERT - - TUMBLING - - TYPE - - UNLIMITED - - UNLOGGED - - UPDATE - - UPSERT - - UNQIESCE - - USER - - SIGNED - - K_STRING_FUNCTION_NAME - UNSIGNED - - VALIDATE - - VALIDATION - - VERBOSE - - VERSION - - VIEW - - VISIBLE - - VOLATILE - - CONCURRENTLY - - WAIT - - WITH TIES - - WITHIN - - WITHOUT - - WITHOUT_ARRAY_WRAPPER - - WORK - - XML - - XMLAGG - - XMLDATA - - XMLSCHEMA - - XMLTEXT - - XSINIL - - YAML - - YES - - ZONE - - -
- - -
         ::= 'ACTION'
-
           | 'ACTIVE'
-
           | 'ADD'
-
           | 'ADVANCE'
-
           | 'ADVISE'
-
           | 'AGAINST'
-
           | 'AGGREGATE'
-
           | 'ALGORITHM'
-
           | 'ALIGN'
-
           | 'ALTER'
-
           | 'ALWAYS'
-
           | 'ANALYZE'
-
           | 'APPEND_ONLY'
-
           | 'APPLY'
-
           | 'APPROXIMATE'
-
           | 'ARCHIVE'
-
           | 'ARRAY'
-
           | 'ASYMMETRIC'
-
           | 'AT'
-
           | 'ASC'
-
           | 'AUTHORIZATION'
-
           | 'AUTO'
-
           | 'AUTO_INCREMENT'
-
           | 'AZURE'
-
           | 'BASE64'
-
           | 'BEFORE'
-
           | 'BEGIN'
-
           | 'BERNOULLI'
-
           | 'BINARY'
-
           | 'BIT'
-
           | 'BLOBSTORAGE'
-
           | 'BLOCK'
-
           | 'BOOLEAN'
-
           | 'BREADTH'
-
           | 'BRANCH'
-
           | 'BROWSE'
-
           | 'BY'
-
           | 'BYTES'
-
           | 'CACHE'
-
           | 'BUFFERS'
-
           | 'BYTE'
-
           | 'CALL'
-
           | 'CASCADE'
-
           | 'CASE'
-
           | 'CAST'
-
           | 'CERTIFICATE'
-
           | 'CHARACTER'
-
           | 'CHANGE'
-
           | 'CHANGES'
-
           | 'CHECKPOINT'
-
           | 'CHAR'
-
           | 'CLOSE'
-
           | 'CLOUD'
-
           | 'COALESCE'
-
           | 'COLLATE'
-
           | 'COLUMN'
-
           | 'COLUMNS'
-
           | 'COMMIT'
-
           | 'COMMENT'
-
           | 'COMMENTS'
-
           | 'CONFLICT'
-
           | 'CONSTRAINTS'
-
           | 'CONVERT'
-
           | 'CORRESPONDING'
-
           | 'COSTS'
-
           | 'COUNT'
-
           | 'CREATED'
-
           | 'CYCLE'
-
           | 'DATABASE'
-
           | 'DATA'
-
           | 'DECLARE'
-
           | 'DBA_RECYCLEBIN'
-
           | 'DEFAULTS'
-
           | 'DEPTH'
-
           | 'DEFERRABLE'
-
           | 'DELAYED'
-
           | 'DELETE'
-
           | 'DELIMIT'
-
           | 'DELIMITER'
-
           | 'DESC'
-
           | 'DESCRIBE'
-
           | 'DISABLE'
-
           | 'DISCARD'
-
           | 'DISCONNECT'
-
           | 'DIV'
-
           | 'DDL'
-
           | 'DML'
-
           | 'DO'
-
           | 'DOMAIN'
-
           | 'DRIVER'
-
           | 'DROP'
-
           | 'DUMP'
-
           | 'DUMPFILE'
-
           | 'DUPLICATE'
-
           | 'ELEMENTS'
-
           | 'ENCLOSED'
-
           | 'EMIT'
-
           | 'EMPTY'
-
           | 'ENABLE'
-
           | 'ENCODING'
-
           | 'ENCRYPTION'
-
           | 'END'
-
           | 'ESCAPED'
-
           | 'ENFORCED'
-
           | 'ENGINE'
-
           | 'ERROR'
-
           | 'ESCAPE'
-
           | 'EXA'
-
           | 'EXCHANGE'
-
           | 'EXCLUDE'
-
           | 'EXCLUDING'
-
           | 'EXCLUSIVE'
-
           | 'EXEC'
-
           | 'EXECUTE'
-
           | 'EXPLAIN'
-
           | 'EXPLICIT'
-
           | 'EXTENDED'
-
           | 'EXTRACT'
-
           | 'EXPORT'
-
           | K_ISOLATION
-
           | 'FILTER'
-
           | 'FIELDS'
-
           | 'FIRST'
-
           | 'FLUSH'
-
           | 'FOLLOWING'
-
           | 'FORMAT'
-
           | 'FULLTEXT'
-
           | 'FUNCTION'
-
           | 'GRANT'
-
           | 'GROUP_CONCAT'
-
           | 'GUARD'
-
           | 'HASH'
-
           | 'HIGH'
-
           | 'HIGH_PRIORITY'
-
           | 'HISTORY'
-
           | 'HOPPING'
-
           | 'IDENTIFIED'
-
           | 'IDENTITY'
-
           | 'INCLUDE'
-
           | 'INCLUDE_NULL_VALUES'
-
           | 'INCLUDING'
-
           | 'INCREMENT'
-
           | 'INDEX'
-
           | 'INFORMATION'
-
           | 'INSERT'
-
           | 'INTERLEAVE'
-
           | 'INTERPRET'
-
           | 'INVALIDATE'
-
           | 'INVERSE'
-
           | 'INVISIBLE'
-
           | 'ISNULL'
-
           | 'JDBC'
-
           | 'JSON'
-
           | 'JSON_OBJECT'
-
           | 'JSON_OBJECTAGG'
-
           | 'JSON_ARRAY'
-
           | 'JSON_ARRAYAGG'
-
           | 'KEEP'
-
           | 'KEY_BLOCK_SIZE'
-
           | 'KEY'
-
           | 'KEYS'
-
           | 'KILL'
-
           | 'FN'
-
           | 'LAST'
-
           | 'LEADING'
-
           | 'LESS'
-
           | 'LEVEL'
-
           | 'LINES'
-
           | 'LOCAL'
-
           | 'LOCK'
-
           | 'LOCKED'
-
           | 'LINK'
-
           | 'LOG'
-
           | 'LOOP'
-
           | 'LOW'
-
           | 'LOW_PRIORITY'
-
           | 'LTRIM'
-
           | 'MATCH'
-
           | 'MATCH_ANY'
-
           | 'MATCH_ALL'
-
           | 'MATCH_PHRASE'
-
           | 'MATCH_PHRASE_PREFIX'
-
           | 'MATCH_REGEXP'
-
           | 'MATCHED'
-
           | 'MATERIALIZED'
-
           | 'MAX'
-
           | 'MAXVALUE'
-
           | 'MEMBER'
-
           | 'MERGE'
-
           | 'MIN'
-
           | 'MINVALUE'
-
           | 'MODE'
-
           | 'MODIFY'
-
           | 'MOVEMENT'
-
           | 'NAMES'
-
           | 'NAME'
-
           | 'NEVER'
-
           | 'NEXT'
-
           | K_NEXTVAL
-
           | 'NO'
-
           | 'NOCACHE'
-
           | 'NOKEEP'
-
           | 'NOLOCK'
-
           | 'NOMAXVALUE'
-
           | 'NOMINVALUE'
-
           | 'NONE'
-
           | 'NOORDER'
-
           | 'NOTHING'
-
           | 'NOTNULL'
-
           | 'NOVALIDATE'
-
           | 'NULLS'
-
           | 'NOWAIT'
-
           | 'OF'
-
           | 'OFF'
-
           | 'OPTIONALLY'
-
           | 'OPEN'
-
           | 'ORA'
-
           | 'ORDINALITY'
-
           | 'OUTFILE'
-
           | 'OVER'
-
           | 'OVERFLOW'
-
           | 'OVERLAPS'
-
           | 'OVERRIDING'
-
           | 'OVERWRITE'
-
           | 'PADDING'
-
           | 'PARALLEL'
-
           | 'PARENT'
-
           | 'PARSER'
-
           | 'PARTITION'
-
           | 'PARTITIONING'
-
           | 'PATH'
-
           | 'PERCENT'
-
           | 'PLACING'
-
           | 'PLAN'
-
           | 'PLUS'
-
           | 'PRECEDING'
-
           | 'PRIMARY'
-
           | 'POLICY'
-
           | 'PURGE'
-
           | 'QUERY'
-
           | 'QUICK'
-
           | 'QUIESCE'
-
           | 'RANGE'
-
           | 'RAW'
-
           | 'READ'
-
           | 'REBUILD'
-
           | 'RECYCLEBIN'
-
           | 'RECURSIVE'
-
           | 'REFERENCES'
-
           | 'REFRESH'
-
           | 'REGEXP'
-
           | 'REJECT'
-
           | 'RESPECT'
-
           | 'RLIKE'
-
           | 'REGEXP_LIKE'
-
           | 'REGISTER'
-
           | 'REMOTE'
-
           | 'REMOVE'
-
           | 'RENAME'
-
           | 'REORGANIZE'
-
           | 'REPAIR'
-
           | 'REPEATABLE'
-
           | 'REPLACE'
-
           | 'RESET'
-
           | 'RESTART'
-
           | 'RESUMABLE'
-
           | 'RESUME'
-
           | 'RESTRICT'
-
           | 'RESTRICTED'
-
           | 'RETURN'
-
           | 'ROLLBACK'
-
           | 'ROLLUP'
-
           | 'ROOT'
-
           | 'ROW'
-
           | 'ROWS'
-
           | 'RTRIM'
-
           | 'SAFE_CAST'
-
           | 'SAFE_CONVERT'
-
           | 'SAVEPOINT'
-
           | 'SCHEMA'
-
           | 'SEARCH'
-
           | 'SECURE'
-
           | 'SECURITY'
-
           | 'SEED'
-
           | 'SEQUENCE'
-
           | 'SEPARATOR'
-
           | 'SESSION'
-
           | 'SETS'
-
           | 'SHOW'
-
           | 'SHUTDOWN'
-
           | 'SHARE'
-
           | 'SIBLINGS'
-
           | 'SIMILAR'
-
           | 'SIZE'
-
           | 'SKIP'
-
           | 'SPATIAL'
-
           | 'STARTING'
-
           | 'STORED'
-
           | 'STREAM'
-
           | 'STRICT'
-
           | 'STRING'
-
           | 'STRUCT'
-
           | 'SUMMARIZE'
-
           | 'SUSPEND'
-
           | 'SWITCH'
-
           | 'SYMMETRIC'
-
           | 'SYNONYM'
-
           | 'SYSTEM'
-
           | 'SYSTEM_TIME'
-
           | 'SYSTEM_TIMESTAMP'
-
           | 'SYSTEM_VERSION'
-
           | 'TABLE'
-
           | 'TABLESPACE'
-
           | 'TERMINATED'
-
           | 'TRIGGER'
-
           | 'THEN'
-
           | 'TEMP'
-
           | K_TEXT_LITERAL
-
           | 'TEMPORARY'
-
           | 'THAN'
-
           | K_TIME_KEY_EXPR
-
           | 'TIMEOUT'
-
           | 'TO'
-
           | 'TRIM'
-
           | 'TRUNCATE'
-
           | 'TRY_CAST'
-
           | 'TRY_CONVERT'
-
           | 'TUMBLING'
-
           | 'TYPE'
-
           | 'UNLIMITED'
-
           | 'UNLOGGED'
-
           | 'UPDATE'
-
           | 'UPSERT'
-
           | 'UNQIESCE'
-
           | 'USER'
-
           | 'SIGNED'
-
           | K_STRING_FUNCTION_NAME
-
           | 'UNSIGNED'
-
           | 'VALIDATE'
-
           | 'VALIDATION'
-
           | 'VERBOSE'
-
           | 'VERSION'
-
           | 'VIEW'
-
           | 'VISIBLE'
-
           | 'VOLATILE'
-
           | 'CONCURRENTLY'
-
           | 'WAIT'
-
           | 'WITH TIES'
-
           | 'WITHIN'
-
           | 'WITHOUT'
-
           | 'WITHOUT_ARRAY_WRAPPER'
-
           | 'WORK'
-
           | 'XML'
-
           | 'XMLAGG'
-
           | 'XMLDATA'
-
           | 'XMLSCHEMA'
-
           | 'XMLTEXT'
-
           | 'XSINIL'
-
           | 'YAML'
-
           | 'YES'
-
           | 'ZONE'
-
- Referenced by: -
- - -====================================================================================================================== -KeywordOrIdentifier -====================================================================================================================== - - -.. raw:: html - - - - - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - NAME - - NEXT - - VALUE - - PUBLIC - - STRING - - DATA - - -
- - -
         ::= S_IDENTIFIER
-
           | S_QUOTED_IDENTIFIER
-
           | 'NAME'
-
           | 'NEXT'
-
           | 'VALUE'
-
           | 'PUBLIC'
-
           | 'STRING'
-
           | 'DATA'
-
- - -====================================================================================================================== -Statement -====================================================================================================================== - - -.. raw:: html - - - - - - IF - - Condition - - SingleStatement - - Block - - ST_SEMICOLON - ELSE - - SingleStatement - - Block - - ST_SEMICOLON - - SingleStatement - - Block - - ST_SEMICOLON - - EOF - - UnsupportedStatement - -
- - -
         ::= 'IF' Condition ( SingleStatement | Block ) ST_SEMICOLON? ( 'ELSE' ( SingleStatement | Block ) ST_SEMICOLON? )?
-
           | ( SingleStatement | Block ) ( ST_SEMICOLON | EOF )
-
           | UnsupportedStatement
-
- Not referenced by any. -
- - -====================================================================================================================== -SingleStatement -====================================================================================================================== - - -.. raw:: html - - - - - - WithList - - SelectWithWithItems - - InsertWithWithItems - - UpdateWithWithItems - - DeleteWithWithItems - - Merge - - Select - - TableStatement - - Upsert - - Alter - - RenameTableStatement - - Create - - Drop - - Analyze - - Truncate - - Execute - - Set - - Reset - - Show - - RefreshMaterializedView - - Use - - SavepointStatement - - RollbackStatement - COMMIT - - Comment - - Describe - - Explain - - Declare - - Grant - - PurgeStatement - - SessionStatement - - LockStatement - - Import - - Export - -
- - - -
           | Select
-
           | TableStatement
-
           | Upsert
-
           | Alter
-
           | RenameTableStatement
-
           | Create
-
           | Drop
-
           | Analyze
-
           | Truncate
-
           | Execute
-
           | Set
-
           | Reset
-
           | Show
-
           | RefreshMaterializedView
-
           | Use
-
           | SavepointStatement
-
           | RollbackStatement
-
           | 'COMMIT'
-
           | Comment
-
           | Describe
-
           | Explain
-
           | Declare
-
           | Grant
-
           | PurgeStatement
-
           | SessionStatement
-
           | LockStatement
-
           | Import
-
           | Export
-
- Referenced by: -
- - -====================================================================================================================== -Block -====================================================================================================================== - - -.. raw:: html - - - - - - BEGIN - - ST_SEMICOLON - - SingleStatement - - Block - - ST_SEMICOLON - END - - ST_SEMICOLON - -
- -
Block    ::= 'BEGIN' ST_SEMICOLON* ( ( SingleStatement | Block ) ST_SEMICOLON )+ 'END' ST_SEMICOLON?
-
- Referenced by: -
- - -====================================================================================================================== -Statements -====================================================================================================================== - - -.. raw:: html - - - - - - ST_SEMICOLON - IF - - Condition - - SingleStatement - - Block - - ST_SEMICOLON - ELSE - - SingleStatement - - Block - - ST_SEMICOLON - - SingleStatement - - Block - - ST_SEMICOLON - - EOF - - ST_SEMICOLON - IF - - Condition - - SingleStatement - - Block - - ST_SEMICOLON - ELSE - - SingleStatement - - Block - - ST_SEMICOLON - - UnsupportedStatement - - EOF - -
- - - -
- Not referenced by any. -
- - -====================================================================================================================== -LockStatement -====================================================================================================================== - - -.. raw:: html - - - - - - LOCK - - TABLE - - Table - IN - - ROW - - SHARE - - EXCLUSIVE - - SHARE - - ROW - - EXCLUSIVE - - UPDATE - - EXCLUSIVE - - MODE - - NOWAIT - - WAIT - - S_LONG - -
- - -
         ::= 'LOCK' 'TABLE' Table 'IN' ( 'ROW' ( 'SHARE' | 'EXCLUSIVE' ) | 'SHARE' ( 'ROW' 'EXCLUSIVE' | 'UPDATE' )? - | 'EXCLUSIVE' ) 'MODE' ( 'NOWAIT' | 'WAIT' S_LONG )?
-
- Referenced by: -
- - -====================================================================================================================== -LikeClause -====================================================================================================================== - - -.. raw:: html - - - - - - LIKE - - Table - ( - - ColumnSelectItemsList - ) - - INCLUDING - - EXCLUDING - - DEFAULTS - - INCLUDING - - EXCLUDING - - IDENTITY - - INCLUDING - - EXCLUDING - - COMMENTS - - -
- - -
         ::= 'LIKE' Table ( '(' ColumnSelectItemsList ')' )? ( ( 'INCLUDING' | 'EXCLUDING' ) 'DEFAULTS' )? ( ( 'INCLUDING' | 'EXCLUDING' - ) 'IDENTITY' )? ( ( 'INCLUDING' | 'EXCLUDING' ) 'COMMENTS' )?
-
- Referenced by: -
- - -====================================================================================================================== -Export -====================================================================================================================== - - -.. raw:: html - - - - - - EXPORT - - Table - - ParenthesedColumnList - - ParenthesedSelect - INTO - - ExportIntoItem - -
- - -
- Referenced by: -
- - -====================================================================================================================== -Import -====================================================================================================================== - - -.. raw:: html - - - - - - IMPORT - - INTO - - Table - - ParenthesedColumnList - - ImportColumns - FROM - - ImportFromItem - -
- -
Import   ::= 'IMPORT' ( 'INTO' ( Table ParenthesedColumnList? | ImportColumns ) )? 'FROM' ImportFromItem
-
- Referenced by: -
- - -====================================================================================================================== -SubImport -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - IMPORT - - INTO - - ImportColumns - FROM - - ImportFromItem - ) - - -
- - -
         ::= '(' 'IMPORT' ( 'INTO' ImportColumns )? 'FROM' ImportFromItem ')'
-
- Referenced by: -
- - -====================================================================================================================== -ImportColumns -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - ColumnDefinition - - LikeClause - , - - ) - - -
- - -
         ::= '(' ( ColumnDefinition | LikeClause ) ( ',' ( ColumnDefinition | LikeClause ) )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -ExportIntoItem -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSDestination - - FileDestination - - ScriptSourceDestination - - ErrorClause - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -ImportFromItem -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSSource - - FileSource - - ScriptSourceDestination - - ErrorClause - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -DBMSDestination -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSType - - ConnectionDefinition - TABLE - - Table - - ParenthesedColumnList - - DBMSTableDestinationOptionList - - ImportExportStatement - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -DBMSTableDestinationOption -====================================================================================================================== - - -.. raw:: html - - - - - - REPLACE - - TRUNCATE - - CREATED - - BY - - S_CHAR_LITERAL - -
- - -
         ::= 'REPLACE'
-
           | 'TRUNCATE'
-
           | 'CREATED' 'BY' S_CHAR_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -DBMSTableDestinationOptionList -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSTableDestinationOption - -
- - -
         ::= DBMSTableDestinationOption+
-
- Referenced by: -
- - -====================================================================================================================== -DBMSSource -====================================================================================================================== - - -.. raw:: html - - - - - - DBMSType - - ConnectionDefinition - TABLE - - Table - - ParenthesedColumnList - - ImportExportStatementsList - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -DBMSType -====================================================================================================================== - - -.. raw:: html - - - - - - EXA - - ORA - - JDBC - - DRIVER - - = - - S_CHAR_LITERAL - -
- -
DBMSType ::= 'EXA'
-
           | 'ORA'
-
           | 'JDBC' ( 'DRIVER' '=' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -FileType -====================================================================================================================== - - -.. raw:: html - - - - - - CSV - - FBV - - -
- -
FileType ::= 'CSV'
-
           | 'FBV'
-
- Referenced by: -
- - -====================================================================================================================== -ImportExportStatement -====================================================================================================================== - - -.. raw:: html - - - - - - STATEMENT - - S_CHAR_LITERAL - -
- - -
         ::= 'STATEMENT' S_CHAR_LITERAL
-
- - -====================================================================================================================== -ImportExportStatementsList -====================================================================================================================== - - -.. raw:: html - - - - - - ImportExportStatement - -
- - -
         ::= ImportExportStatement+
-
- Referenced by: -
- - -====================================================================================================================== -File -====================================================================================================================== - - -.. raw:: html - - - - - - FILE - - S_CHAR_LITERAL - -
- -
File     ::= 'FILE' S_CHAR_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -FileList -====================================================================================================================== - - -.. raw:: html - - - - - - File - -
- - -
- - -====================================================================================================================== -ConnectionFileDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - ConnectionOrCloudConnectionDefinition - - FileList - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -ConnectionFileDefinitionList -====================================================================================================================== - - -.. raw:: html - - - - - - ConnectionFileDefinition - -
- - -
         ::= ConnectionFileDefinition+
-
- Referenced by: -
- - -====================================================================================================================== -CSVDestinationColumn -====================================================================================================================== - - -.. raw:: html - - - - - - S_LONG - .. - - S_LONG - FORMAT - - = - - S_CHAR_LITERAL - DELIMIT - - = - - ALWAYS - - NEVER - - AUTO - - -
- - -
         ::= S_LONG ( '..' S_LONG | ( 'FORMAT' '=' S_CHAR_LITERAL )? ( 'DELIMIT' '=' ( 'ALWAYS' | 'NEVER' | 'AUTO' ) )? )
-
- Referenced by: -
- - -====================================================================================================================== -CSVDestinationColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - CSVDestinationColumn - , - - -
- - -
         ::= CSVDestinationColumn ( ',' CSVDestinationColumn )*
-
- Referenced by: -
- - -====================================================================================================================== -CSVSourceColumn -====================================================================================================================== - - -.. raw:: html - - - - - - S_LONG - .. - - S_LONG - FORMAT - - = - - S_CHAR_LITERAL - -
- - -
         ::= S_LONG ( '..' S_LONG | 'FORMAT' '=' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -CSVSourceColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - CSVSourceColumn - , - - -
- - -
         ::= CSVSourceColumn ( ',' CSVSourceColumn )*
-
- Referenced by: -
- - -====================================================================================================================== -FBVDestinationColumn -====================================================================================================================== - - -.. raw:: html - - - - - - SIZE - - = - - S_LONG - FORMAT - - PADDING - - = - - S_CHAR_LITERAL - ALIGN - - = - - LEFT - - RIGHT - - -
- - -
         ::= 'SIZE' '=' S_LONG
-
           | ( 'FORMAT' | 'PADDING' ) '=' S_CHAR_LITERAL
-
           | 'ALIGN' '=' ( 'LEFT' | 'RIGHT' )
-
- Referenced by: -
- - -====================================================================================================================== -FBVDestinationColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - FBVDestinationColumn - , - - -
- - -
         ::= FBVDestinationColumn ( ','? FBVDestinationColumn )*
-
- Referenced by: -
- - -====================================================================================================================== -FBVSourceColumn -====================================================================================================================== - - -.. raw:: html - - - - - - SIZE - - START - - = - - S_LONG - FORMAT - - PADDING - - = - - S_CHAR_LITERAL - ALIGN - - = - - LEFT - - RIGHT - - -
- - -
         ::= ( 'SIZE' | 'START' ) '=' S_LONG
-
           | ( 'FORMAT' | 'PADDING' ) '=' S_CHAR_LITERAL
-
           | 'ALIGN' '=' ( 'LEFT' | 'RIGHT' )
-
- Referenced by: -
- - -====================================================================================================================== -FBVSourceColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - FBVSourceColumn - , - - -
- - -
         ::= FBVSourceColumn ( ','? FBVSourceColumn )*
-
- Referenced by: -
- - -====================================================================================================================== -FileDestinationOption -====================================================================================================================== - - -.. raw:: html - - - - - - REPLACE - - TRUNCATE - - WITH - - COLUMN - - NAMES - - ENCODING - - NULL - - BOOLEAN - - ROW - - SEPARATOR - - COLUMN - - SEPARATOR - - DELIMITER - - = - - S_CHAR_LITERAL - DELIMIT - - = - - ALWAYS - - NEVER - - AUTO - - -
- - -
         ::= 'REPLACE'
-
           | 'TRUNCATE'
-
           | 'WITH' 'COLUMN' 'NAMES'
-
           | ( 'ENCODING' | 'NULL' | 'BOOLEAN' | 'ROW' 'SEPARATOR' | 'COLUMN' ( 'SEPARATOR' - | 'DELIMITER' ) ) '=' S_CHAR_LITERAL
-
           | 'DELIMIT' '=' ( 'ALWAYS' | 'NEVER' | 'AUTO' )
-
- Referenced by: -
- - -====================================================================================================================== -FileDestinationOptionList -====================================================================================================================== - - -.. raw:: html - - - - - - FileDestinationOption - -
- - -
         ::= FileDestinationOption+
-
- Referenced by: -
- - -====================================================================================================================== -FileSourceOption -====================================================================================================================== - - -.. raw:: html - - - - - - TRIM - - LTRIM - - RTRIM - - ENCODING - - NULL - - COLUMN - - SEPARATOR - - DELIMITER - - = - - S_CHAR_LITERAL - SKIP - - = - - S_LONG - ROW - - SEPARATOR - - = - - S_CHAR_LITERAL - SIZE - - = - - S_LONG - -
- - -
         ::= 'TRIM'
-
           | 'LTRIM'
-
           | 'RTRIM'
-
           | ( 'ENCODING' | 'NULL' | 'COLUMN' ( 'SEPARATOR' | 'DELIMITER' ) ) '=' - S_CHAR_LITERAL
-
           | 'SKIP' '=' S_LONG
-
           | 'ROW' ( 'SEPARATOR' '=' S_CHAR_LITERAL | 'SIZE' '=' S_LONG )
-
- Referenced by: -
- - -====================================================================================================================== -FileSourceOptionList -====================================================================================================================== - - -.. raw:: html - - - - - - FileSourceOption - -
- - -
         ::= FileSourceOption+
-
- Referenced by: -
- - -====================================================================================================================== -FileDestination -====================================================================================================================== - - -.. raw:: html - - - - - - FileType - - ConnectionFileDefinitionList - LOCAL - - SECURE - - FileType - - FileList - ( - - CSVDestinationColumnList - - FBVDestinationColumnList - ) - - FileDestinationOptionList - - CertificateVerification - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -FileSource -====================================================================================================================== - - -.. raw:: html - - - - - - FileType - - ConnectionFileDefinitionList - LOCAL - - SECURE - - FileType - - FileList - ( - - CSVSourceColumnList - - FBVSourceColumnList - ) - - FileSourceOptionList - - CertificateVerification - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -CertificateVerification -====================================================================================================================== - - -.. raw:: html - - - - - - IGNORE - - VERIFY - - CERTIFICATE - - PUBLIC - - KEY - - S_CHAR_LITERAL - PUBLIC - - KEY - - S_CHAR_LITERAL - -
- - -
         ::= ( 'IGNORE' | 'VERIFY' ) 'CERTIFICATE' ( 'PUBLIC' 'KEY' S_CHAR_LITERAL )?
-
           | 'PUBLIC' 'KEY' S_CHAR_LITERAL
-
- - -====================================================================================================================== -ScriptSourceDestination -====================================================================================================================== - - -.. raw:: html - - - - - - SCRIPT - - Table - - ConnectionDefinition - WITH - - RelObjectName - = - - S_CHAR_LITERAL - -
- - -
         ::= 'SCRIPT' Table ConnectionDefinition? ( 'WITH' ( RelObjectName '=' S_CHAR_LITERAL )+ )?
-
- Referenced by: -
- - -====================================================================================================================== -UserIdentification -====================================================================================================================== - - -.. raw:: html - - - - - - USER - - S_CHAR_LITERAL - IDENTIFIED - - BY - - S_CHAR_LITERAL - -
- - -
         ::= 'USER' S_CHAR_LITERAL 'IDENTIFIED' 'BY' S_CHAR_LITERAL
-
- - -====================================================================================================================== -ConnectionDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - AT - - RelObjectName - - S_CHAR_LITERAL - - UserIdentification - - CertificateVerification - -
- - - -
- - -====================================================================================================================== -CloudConnectionDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - AT - - CLOUD - - NONE - - AZURE - - BLOBSTORAGE - - RelObjectName - - S_CHAR_LITERAL - - UserIdentification - -
- - -
         ::= 'AT' 'CLOUD' ( 'NONE' | 'AZURE' 'BLOBSTORAGE' ) ( RelObjectName | S_CHAR_LITERAL ) UserIdentification?
-
- - -====================================================================================================================== -ConnectionOrCloudConnectionDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - CloudConnectionDefinition - - ConnectionDefinition - -
- - -
         ::= CloudConnectionDefinition
-
           | ConnectionDefinition
-
- - -====================================================================================================================== -ErrorClause -====================================================================================================================== - - -.. raw:: html - - - - - - ERRORS - - INTO - - ErrorDestination - ( - - Expression - ) - - REPLACE - - TRUNCATE - - RejectClause - - RejectClause - -
- - -
         ::= 'ERRORS' 'INTO' ErrorDestination ( '(' Expression ')' )? ( 'REPLACE' | 'TRUNCATE' )? RejectClause?
-
           | RejectClause
-
- Referenced by: -
- - -====================================================================================================================== -RejectClause -====================================================================================================================== - - -.. raw:: html - - - - - - REJECT - - LIMIT - - S_LONG - UNLIMITED - - ERRORS - - -
- - -
         ::= 'REJECT' 'LIMIT' ( S_LONG | 'UNLIMITED' ) 'ERRORS'?
-
- Referenced by: -
- - -====================================================================================================================== -ErrorDestination -====================================================================================================================== - - -.. raw:: html - - - - - - CSVFileDestination - - Table - -
- - -
         ::= CSVFileDestination
-
           | Table
-
- Referenced by: -
- - -====================================================================================================================== -CSVFileDestination -====================================================================================================================== - - -.. raw:: html - - - - - - CSV - - ConnectionOrCloudConnectionDefinition - LOCAL - - SECURE - - CSV - - File - -
- - -
         ::= ( 'CSV' ConnectionOrCloudConnectionDefinition | 'LOCAL' 'SECURE'? 'CSV' ) File
-
- Referenced by: -
- - -====================================================================================================================== -Declare -====================================================================================================================== - - -.. raw:: html - - - - - - DECLARE - - UserVariable - TABLE - - ( - - ColumnDefinition - , - - ) - - AS - - RelObjectName - - ColDataType - = - - Expression - - UserVariable - , - - -
- -
Declare  ::= 'DECLARE' UserVariable ( 'TABLE' '(' ColumnDefinition ( ',' ColumnDefinition )* ')' | 'AS' RelObjectName | ColDataType ( '=' Expression )? ( ',' UserVariable ColDataType ( '=' Expression )? )* )
-
- Referenced by: -
- - -====================================================================================================================== -SessionStatement -====================================================================================================================== - - -.. raw:: html - - - - - - SESSION - - BRANCH - - START - - APPLY - - DROP - - SHOW - - DESCRIBE - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - S_CHAR_LITERAL - - S_LONG - . - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - S_CHAR_LITERAL - - S_LONG - WITH - - S_IDENTIFIER - KEEP - - = - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - S_CHAR_LITERAL - - S_LONG - TRUE - - FALSE - - ON - - OFF - - YES - - NO - - , - - -
- - -
         ::= ( 'SESSION' | 'BRANCH' ) ( 'START' | 'APPLY' | 'DROP' | 'SHOW' | 'DESCRIBE' - ) ( ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | S_CHAR_LITERAL | S_LONG ) ( '.' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | S_CHAR_LITERAL | S_LONG ) )? )? ( 'WITH' ( S_IDENTIFIER | 'KEEP' ) '=' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | S_CHAR_LITERAL | S_LONG | 'TRUE' | 'FALSE' | 'ON' | 'OFF' | 'YES' | 'NO' ) ( ',' ( S_IDENTIFIER | 'KEEP' ) '=' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | S_CHAR_LITERAL | S_LONG | 'TRUE' | 'FALSE' | 'ON' | 'OFF' | 'YES' | 'NO' ) )* )?
-
- Referenced by: -
- - -====================================================================================================================== -Set -====================================================================================================================== - - -.. raw:: html - - - - - - SET - - LOCAL - - SESSION - - K_DATETIMELITERAL - ZONE - - UserVariable - - IdentifierChain - = - - Expression - ZONE - - K_DATETIMELITERAL - = - - RelObjectName - , - - -
- -
Set      ::= 'SET' ( 'LOCAL' | 'SESSION' )? ( K_DATETIMELITERAL 'ZONE' | ( UserVariable | IdentifierChain ) '='? ) Expression ( ',' ( K_DATETIMELITERAL 'ZONE' | RelObjectName '='? )? Expression )*
-
- Referenced by: -
- - -====================================================================================================================== -Reset -====================================================================================================================== - - -.. raw:: html - - - - - - RESET - - K_DATETIMELITERAL - ZONE - - RelObjectName - ALL - - -
- -
Reset    ::= 'RESET' ( K_DATETIMELITERAL 'ZONE' | RelObjectName | 'ALL' )
-
- Referenced by: -
- - -====================================================================================================================== -RenameTableStatement -====================================================================================================================== - - -.. raw:: html - - - - - - RENAME - - TABLE - - IF - - EXISTS - - Table - WAIT - - S_LONG - NOWAIT - - TO - - Table - - Table - , - - -
- - -
         ::= 'RENAME' 'TABLE'? ( 'IF' 'EXISTS' )? Table ( 'WAIT' S_LONG | 'NOWAIT' )? 'TO' Table ( ',' Table 'TO' Table )*
-
- Referenced by: -
- - -====================================================================================================================== -PurgeStatement -====================================================================================================================== - - -.. raw:: html - - - - - - PURGE - - TABLE - - Table - INDEX - - Index - RECYCLEBIN - - DBA_RECYCLEBIN - - TABLESPACE - - S_IDENTIFIER - USER - - S_IDENTIFIER - -
- - -
         ::= 'PURGE' ( 'TABLE' Table | 'INDEX' Index | 'RECYCLEBIN' | 'DBA_RECYCLEBIN' | 'TABLESPACE' S_IDENTIFIER ( 'USER' S_IDENTIFIER )? )
-
- Referenced by: -
- - -====================================================================================================================== -Describe -====================================================================================================================== - - -.. raw:: html - - - - - - DESCRIBE - - DESC - - Table - -
- -
Describe ::= ( 'DESCRIBE' | 'DESC' ) Table
-
- Referenced by: -
- - -====================================================================================================================== -Explain -====================================================================================================================== - - -.. raw:: html - - - - - - EXPLAIN - - SUMMARIZE - - ExplainStatementOptions - - WithList - - SelectWithWithItems - - InsertWithWithItems - - UpdateWithWithItems - - DeleteWithWithItems - - Merge - - Table - -
- - -
- Referenced by: -
- - -====================================================================================================================== -ExplainOptionBoolean -====================================================================================================================== - - -.. raw:: html - - - - - - TRUE - - FALSE - - ON - - OFF - - -
- - -
         ::= ( 'TRUE' | 'FALSE' | 'ON' | 'OFF' )?
-
- Referenced by: -
- - -====================================================================================================================== -ExplainFormatOption -====================================================================================================================== - - -.. raw:: html - - - - - - XML - - JSON - - YAML - - -
- - -
         ::= ( 'XML' | 'JSON' | 'YAML' )?
-
- Referenced by: -
- - -====================================================================================================================== -ExplainStatementOptions -====================================================================================================================== - - -.. raw:: html - - - - - - ANALYZE - - BUFFERS - - COSTS - - VERBOSE - - ExplainOptionBoolean - FORMAT - - PLAN - - FOR - - ExplainFormatOption - -
- - -
         ::= ( ( 'ANALYZE' | 'BUFFERS' | 'COSTS' | 'VERBOSE' ) ExplainOptionBoolean | ( 'FORMAT' | 'PLAN' 'FOR'? ) ExplainFormatOption )*
-
- Referenced by: -
- - -====================================================================================================================== -Use -====================================================================================================================== - - -.. raw:: html - - - - - - USE - - SCHEMA - - RelObjectName - -
- -
Use      ::= 'USE' 'SCHEMA'? RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -Show -====================================================================================================================== - - -.. raw:: html - - - - - - SHOW - - ShowColumns - - ShowIndex - - ShowTables - - captureRest - -
- -
Show     ::= 'SHOW' ( ShowColumns | ShowIndex | ShowTables | captureRest )
-
- Referenced by: -
- - -====================================================================================================================== -ShowColumns -====================================================================================================================== - - -.. raw:: html - - - - - - COLUMNS - - FROM - - RelObjectName - -
- - -
         ::= 'COLUMNS' 'FROM' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -ShowIndex -====================================================================================================================== - - -.. raw:: html - - - - - - INDEX - - FROM - - RelObjectName - -
- - -
         ::= 'INDEX' 'FROM' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -RefreshMaterializedView -====================================================================================================================== - - -.. raw:: html - - - - - - REFRESH - - MATERIALIZED - - VIEW - - CONCURRENTLY - - Table - WITH - - NO - - DATA - - captureRest - -
- - -
         ::= 'REFRESH' 'MATERIALIZED' 'VIEW' 'CONCURRENTLY'? Table ( 'WITH' 'NO'? 'DATA' )? captureRest
-
- Referenced by: -
- - -====================================================================================================================== -ShowTables -====================================================================================================================== - - -.. raw:: html - - - - - - EXTENDED - - FULL - - TABLES - - FROM - - IN - - RelObjectName - LIKE - - SimpleExpression - WHERE - - Expression - -
- - -
         ::= 'EXTENDED'? 'FULL'? 'TABLES' ( ( 'FROM' | 'IN' ) RelObjectName )? ( 'LIKE' SimpleExpression | 'WHERE' Expression )?
-
- Referenced by: -
- - -====================================================================================================================== -Values -====================================================================================================================== - - -.. raw:: html - - - - - - VALUES - - VALUE - - ExpressionList - -
- -
Values   ::= ( 'VALUES' | 'VALUE' ) ExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -ReturningClause -====================================================================================================================== - - -.. raw:: html - - - - - - RETURNING - - RETURN - - ReturningOutputAliasList - - SelectItemsList - INTO - - Table - - UserVariable - , - - -
- - -
         ::= ( 'RETURNING' | 'RETURN' ) ReturningOutputAliasList? SelectItemsList ( 'INTO' ( Table | UserVariable ) ( ',' ( Table | UserVariable ) )* )?
-
- Referenced by: -
- - -====================================================================================================================== -ReturningReferenceKind -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - -
- - -
         ::= RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -ReturningOutputAliasDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - ReturningReferenceKind - AS - - RelObjectName - -
- - -
         ::= ReturningReferenceKind 'AS' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -ReturningOutputAliasList -====================================================================================================================== - - -.. raw:: html - - - - - - WITH - - ( - - ReturningOutputAliasDefinition - , - - ) - - -
- - -
         ::= 'WITH' '(' ReturningOutputAliasDefinition ( ',' ReturningOutputAliasDefinition )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -UpdateWithWithItems -====================================================================================================================== - - -.. raw:: html - - - - - - Update - -
- - -
         ::= Update
-
- Referenced by: -
- - -====================================================================================================================== -Update -====================================================================================================================== - - -.. raw:: html - - - - - - UPDATE - - LOW_PRIORITY - - IGNORE - - TableWithAliasAndMysqlIndexHint - - JoinsList - SET - - UpdateSets - - OutputClause - FROM - - FromItem - - JoinsList - - WhereClause - - PreferringClause - - OrderByElements - - PlainLimit - - ReturningClause - -
- - -
- - -====================================================================================================================== -UpdateSets -====================================================================================================================== - - -.. raw:: html - - - - - - Column - = - - Expression - - ParenthesedExpressionList - = - - ParenthesedSelect - - ParenthesedExpressionList - , - - Column - = - - Expression - - ParenthesedExpressionList - = - - ParenthesedSelect - - ParenthesedExpressionList - -
- - - -
- - -====================================================================================================================== -Partitions -====================================================================================================================== - - -.. raw:: html - - - - - - Column - = - - Expression - , - - -
- - -
         ::= Column ( '=' Expression )? ( ',' Column ( '=' Expression )? )*
-
- Referenced by: -
- - -====================================================================================================================== -InsertWithWithItems -====================================================================================================================== - - -.. raw:: html - - - - - - Insert - -
- - -
         ::= Insert
-
- Referenced by: -
- - -====================================================================================================================== -Insert -====================================================================================================================== - - -.. raw:: html - - - - - - INSERT - - LOW_PRIORITY - - DELAYED - - HIGH_PRIORITY - - IGNORE - - ALL - - FIRST - - OracleMultiInsertClause - - OracleMultiInsertWhenBranch - - OracleMultiInsertElseBranch - - Select - OVERWRITE - - TABLE - - INTO - - TABLE - - Table - PARTITION - - ( - - Partitions - ) - - AS - - RelObjectName - ( - - ColumnList - ) - - OVERRIDING - - SYSTEM - - VALUE - - OutputClause - DEFAULT - - VALUES - - SET - - UpdateSets - - Select - - Alias - ON - - DUPLICATE - - KEY - - UPDATE - - InsertDuplicateAction - ON - - CONFLICT - - InsertConflictTarget - - InsertConflictAction - - ReturningClause - -
- -
Insert   ::= 'INSERT' ( 'LOW_PRIORITY' | 'DELAYED' | 'HIGH_PRIORITY' )? 'IGNORE'? ( ( 'ALL' - | 'FIRST' ) ( OracleMultiInsertClause+ | OracleMultiInsertWhenBranch+ OracleMultiInsertElseBranch? ) Select | ( 'OVERWRITE' 'TABLE' | 'INTO' 'TABLE'? )? Table ( 'PARTITION' '(' Partitions ')' )? ( 'AS'? RelObjectName )? ( '(' ColumnList ')' )? ( 'OVERRIDING' 'SYSTEM' 'VALUE' )? OutputClause? ( 'DEFAULT' 'VALUES' | 'SET' UpdateSets | Select ) Alias? ( 'ON' 'DUPLICATE' 'KEY' 'UPDATE' InsertDuplicateAction )? ( 'ON' 'CONFLICT' InsertConflictTarget? InsertConflictAction )? ReturningClause? )
-
- - -====================================================================================================================== -OracleMultiInsertClause -====================================================================================================================== - - -.. raw:: html - - - - - - INTO - - Table - ( - - ColumnList - ) - - Select - -
- - -
         ::= 'INTO' Table ( '(' ColumnList ')' )? Select
-
- - -====================================================================================================================== -OracleMultiInsertWhenBranch -====================================================================================================================== - - -.. raw:: html - - - - - - WHEN - - Expression - THEN - - OracleMultiInsertClauseList - -
- - -
         ::= 'WHEN' Expression 'THEN' OracleMultiInsertClauseList
-
- Referenced by: -
- - -====================================================================================================================== -OracleMultiInsertElseBranch -====================================================================================================================== - - -.. raw:: html - - - - - - ELSE - - OracleMultiInsertClauseList - -
- - -
         ::= 'ELSE' OracleMultiInsertClauseList
-
- Referenced by: -
- - -====================================================================================================================== -OracleMultiInsertClauseList -====================================================================================================================== - - -.. raw:: html - - - - - - OracleMultiInsertClause - -
- - -
         ::= OracleMultiInsertClause+
-
- - -====================================================================================================================== -InsertConflictTarget -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - RelObjectNameExt - , - - ) - - WhereClause - ON - - CONSTRAINT - - RelObjectNameExt - -
- - -
         ::= '(' RelObjectNameExt ( ',' RelObjectNameExt )* ')' WhereClause?
-
           | 'ON' 'CONSTRAINT' RelObjectNameExt
-
- Referenced by: -
- - -====================================================================================================================== -InsertConflictAction -====================================================================================================================== - - -.. raw:: html - - - - - - DO - - NOTHING - - UPDATE - - SET - - UpdateSets - - WhereClause - -
- - -
         ::= 'DO' ( 'NOTHING' | 'UPDATE' 'SET' UpdateSets WhereClause? )
-
- Referenced by: -
- - -====================================================================================================================== -InsertDuplicateAction -====================================================================================================================== - - -.. raw:: html - - - - - - NOTHING - - UpdateSets - - WhereClause - -
- - -
         ::= 'NOTHING'
-
           | UpdateSets WhereClause?
-
- Referenced by: -
- - -====================================================================================================================== -OutputClause -====================================================================================================================== - - -.. raw:: html - - - - - - OUTPUT - - SelectItemsList - INTO - - UserVariable - - Table - - ColumnsNamesList - -
- - -
         ::= 'OUTPUT' SelectItemsList ( 'INTO' ( UserVariable | Table ) ColumnsNamesList? )?
-
- Referenced by: -
- - -====================================================================================================================== -Upsert -====================================================================================================================== - - -.. raw:: html - - - - - - UPSERT - - INSERT - - OR - - REPLACE - - INTO - - Table - - ParenthesedColumnList - SET - - UpdateSets - - Select - ON - - DUPLICATE - - KEY - - UPDATE - - InsertDuplicateAction - -
- -
Upsert   ::= ( 'UPSERT' | ( 'INSERT' 'OR' )? 'REPLACE' ) 'INTO'? Table ParenthesedColumnList? ( 'SET' UpdateSets | Select ) ( 'ON' 'DUPLICATE' 'KEY' 'UPDATE' InsertDuplicateAction )?
-
- Referenced by: -
- - -====================================================================================================================== -DeleteWithWithItems -====================================================================================================================== - - -.. raw:: html - - - - - - Delete - -
- - -
         ::= Delete
-
- Referenced by: -
- - -====================================================================================================================== -Delete -====================================================================================================================== - - -.. raw:: html - - - - - - DELETE - - LOW_PRIORITY - - QUICK - - IGNORE - - TableWithAlias - , - - OutputClause - FROM - - TableWithAlias - - JoinsList - USING - - FromItem - , - - WhereClause - - PreferringClause - - OrderByElements - - PlainLimit - - ReturningClause - -
- -
Delete   ::= 'DELETE' 'LOW_PRIORITY'? 'QUICK'? 'IGNORE'? ( ( TableWithAlias ( ',' TableWithAlias )* OutputClause? )? 'FROM' )? ( TableWithAlias JoinsList? )? ( 'USING' FromItem ( ',' FromItem )* )? WhereClause? PreferringClause? OrderByElements? PlainLimit? ReturningClause?
-
- - -====================================================================================================================== -Merge -====================================================================================================================== - - -.. raw:: html - - - - - - MERGE - - INTO - - TableWithAlias - USING - - FromItem - ON - - Expression - - MergeOperations - - OutputClause - -
- -
Merge    ::= 'MERGE' 'INTO' TableWithAlias 'USING' FromItem 'ON' Expression MergeOperations OutputClause?
-
- Referenced by: -
- - -====================================================================================================================== -MergeOperations -====================================================================================================================== - - -.. raw:: html - - - - - - MergeWhenMatched - - MergeWhenNotMatched - -
- - -
         ::= ( MergeWhenMatched | MergeWhenNotMatched )*
-
- Referenced by: -
- - -====================================================================================================================== -MergeWhenMatched -====================================================================================================================== - - -.. raw:: html - - - - - - WHEN - - MATCHED - - AND - - Expression - THEN - - DELETE - - MergeUpdateClause - -
- - -
         ::= 'WHEN' 'MATCHED' ( 'AND' Expression )? 'THEN' ( 'DELETE' | MergeUpdateClause )
-
- Referenced by: -
- - -====================================================================================================================== -MergeUpdateClause -====================================================================================================================== - - -.. raw:: html - - - - - - UPDATE - - SET - - UpdateSets - WHERE - - Expression - DELETE - - WHERE - - Expression - -
- - -
         ::= 'UPDATE' 'SET' UpdateSets ( 'WHERE' Expression )? ( 'DELETE' 'WHERE' Expression )?
-
- Referenced by: -
- - -====================================================================================================================== -MergeWhenNotMatched -====================================================================================================================== - - -.. raw:: html - - - - - - WHEN - - NOT - - MATCHED - - AND - - Expression - THEN - - INSERT - - ( - - ColumnList - ) - - VALUES - - ( - - SimpleExpressionList - ) - - WHERE - - Expression - -
- - -
         ::= 'WHEN' 'NOT' 'MATCHED' ( 'AND' Expression )? 'THEN' 'INSERT' ( '(' ColumnList ')' )? 'VALUES' '(' SimpleExpressionList ')' ( 'WHERE' Expression )?
-
- Referenced by: -
- - -====================================================================================================================== -RelObjectNames -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ... - - .. - - . - - : - - RelObjectNameExt - -
- - -
         ::= RelObjectName ( ( '...' | '..' | '.' | ':' ) RelObjectNameExt )*
-
- - -====================================================================================================================== -ColumnIdentifier -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ... - - .. - - . - - RelObjectNameExt - -
- - -
         ::= RelObjectName ( ( '...' | '..' | '.' ) RelObjectNameExt )*
-
- Referenced by: -
- - -====================================================================================================================== -Column -====================================================================================================================== - - -.. raw:: html - - - - - - ColumnIdentifier - COMMENT - - S_CHAR_LITERAL - . - - K_NEXTVAL - - ArrayConstructor - -
- - -
- - -====================================================================================================================== -RelObjectName -====================================================================================================================== - - -.. raw:: html - - - - - - DATA_TYPE - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - K_DATETIMELITERAL - - K_DATE_LITERAL - - NonReservedWord - ALL - - ANY - - CASEWHEN - - CONNECT - - CREATE - - DEFAULT - - GLOBAL - - GROUP - - GROUPING - - IF - - IIF - - IGNORE - - IN - - INTERVAL - - LEFT - - LIMIT - - K_NEXTVAL - OFFSET - - ON - - OPTIMIZE - - ORDER - - PROCEDURE - - PUBLIC - - QUALIFY - - RIGHT - - SET - - SOME - - START - - TABLES - - TOP - - VALUE - - VALUES - - -
- - -
         ::= DATA_TYPE
-
           | S_IDENTIFIER
-
           | S_QUOTED_IDENTIFIER
-
           | K_DATETIMELITERAL
-
           | K_DATE_LITERAL
-
           | NonReservedWord
-
           | 'ALL'
-
           | 'ANY'
-
           | 'CASEWHEN'
-
           | 'CONNECT'
-
           | 'CREATE'
-
           | 'DEFAULT'
-
           | 'GLOBAL'
-
           | 'GROUP'
-
           | 'GROUPING'
-
           | 'IF'
-
           | 'IIF'
-
           | 'IGNORE'
-
           | 'IN'
-
           | 'INTERVAL'
-
           | 'LEFT'
-
           | 'LIMIT'
-
           | K_NEXTVAL
-
           | 'OFFSET'
-
           | 'ON'
-
           | 'OPTIMIZE'
-
           | 'ORDER'
-
           | 'PROCEDURE'
-
           | 'PUBLIC'
-
           | 'QUALIFY'
-
           | 'RIGHT'
-
           | 'SET'
-
           | 'SOME'
-
           | 'START'
-
           | 'TABLES'
-
           | 'TOP'
-
           | 'VALUE'
-
           | 'VALUES'
-
- - -====================================================================================================================== -RelObjectNameExt -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - FROM - - K_SELECT - CURRENT - - -
- - -
         ::= RelObjectName
-
           | 'FROM'
-
           | K_SELECT
-
           | 'CURRENT'
-
- - -====================================================================================================================== -Table -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNames - - TimeTravelBeforeAlias - - S_CHAR_LITERAL - -
- - -
           | S_CHAR_LITERAL
-
- - -====================================================================================================================== -TableWithAlias -====================================================================================================================== - - -.. raw:: html - - - - - - Table - - Alias - -
- - -
         ::= Table Alias?
-
- Referenced by: -
- - -====================================================================================================================== -TableWithAliasAndMysqlIndexHint -====================================================================================================================== - - -.. raw:: html - - - - - - Table - - Alias - - MySQLIndexHint - -
- - -
         ::= Table Alias? MySQLIndexHint?
-
- Referenced by: -
- - -====================================================================================================================== -Number -====================================================================================================================== - - -.. raw:: html - - - - - - S_DOUBLE - - S_LONG - -
- -
Number   ::= S_DOUBLE
-
           | S_LONG
-
- Referenced by: -
- - -====================================================================================================================== -SampleClause -====================================================================================================================== - - -.. raw:: html - - - - - - SAMPLE - - BLOCK - - TABLESAMPLE - - USING - - SAMPLE - - SYSTEM - - BERNOULLI - - ( - - Number - % - - PERCENT - - ROWS - - ) - - REPEATABLE - - ( - - Number - ) - - SEED - - ( - - Number - ) - - Number - OFFSET - - Number - -
- - -
         ::= ( 'SAMPLE' 'BLOCK'? | ( 'TABLESAMPLE' | 'USING' 'SAMPLE' ) ( 'SYSTEM' - | 'BERNOULLI' ) ) ( '(' Number ( '%' | 'PERCENT' | 'ROWS' )? ')' ( 'REPEATABLE' '(' Number ')' )? ( 'SEED' '(' Number ')' )? | Number ( 'OFFSET' Number )? )
-
- Referenced by: -
- - -====================================================================================================================== -SelectWithWithItems -====================================================================================================================== - - -.. raw:: html - - - - - - Select - -
- - -
         ::= Select
-
- Referenced by: -
- - -====================================================================================================================== -Select -====================================================================================================================== - - -.. raw:: html - - - - - - WithList - - FromQuery - - PlainSelect - - Values - - ParenthesedSelect - - Alias - - FromQueryFromSelect - - SetOperationList - - OrderByElements - - LimitWithOffset - - Offset - - Fetch - - WithIsolation - -
- - -
- - -====================================================================================================================== -FromQuery -====================================================================================================================== - - -.. raw:: html - - - - - - FROM - - FromItem - - LateralViews - - JoinsList - |> - - PipeOperator - -
- - -
         ::= 'FROM' FromItem LateralViews? JoinsList? ( '|>' PipeOperator )*
-
- Referenced by: -
- - -====================================================================================================================== -FromQueryFromSelect -====================================================================================================================== - - -.. raw:: html - - - - - - |> - - PipeOperator - -
- - -
         ::= ( '|>' PipeOperator )+
-
- Referenced by: -
- - -====================================================================================================================== -PipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - SelectPipeOperator - - SetPipeOperator - - DropPipeOperator - - AsPipeOperator - - WherePipeOperator - - LimitPipeOperator - - AggregatePipeOperator - - OrderByPipeOperator - - SetOperationPipeOperator - - JoinPipeOperator - - CallPipeOperator - - TableSamplePipeOperator - - PivotPipeOperator - - UnPivotPipeOperator - -
- - -
         ::= SelectPipeOperator
-
           | SetPipeOperator
-
           | DropPipeOperator
-
           | AsPipeOperator
-
           | WherePipeOperator
-
           | LimitPipeOperator
-
           | AggregatePipeOperator
-
           | OrderByPipeOperator
-
           | SetOperationPipeOperator
-
           | JoinPipeOperator
-
           | CallPipeOperator
-
           | TableSamplePipeOperator
-
           | PivotPipeOperator
-
           | UnPivotPipeOperator
-
- Referenced by: -
- - -====================================================================================================================== -SelectPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - K_SELECT - DISTINCT - - ALL - - EXTEND - - WINDOW - - RENAME - - SelectItem - , - - -
- - -
         ::= ( K_SELECT ( 'DISTINCT' | 'ALL' )? | 'EXTEND' | 'WINDOW' | 'RENAME' ) SelectItem ( ',' SelectItem )*
-
- Referenced by: -
- - -====================================================================================================================== -WherePipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - WHERE - - Expression - -
- - -
         ::= 'WHERE' Expression
-
- Referenced by: -
- - -====================================================================================================================== -OrderSuffix -====================================================================================================================== - - -.. raw:: html - - - - - - ASC - - DESC - - NULLS - - FIRST - - LAST - - -
- - -
         ::= ( 'ASC' | 'DESC' ) ( 'NULLS' ( 'FIRST' | 'LAST' ) )?
-
- Referenced by: -
- - -====================================================================================================================== -AggregatePipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - AGGREGATE - - SelectItem - - OrderSuffix - , - - GROUP - - AND - - ORDER - - BY - - SelectItem - - OrderSuffix - , - - -
- - -
         ::= 'AGGREGATE' SelectItem OrderSuffix? ( ',' SelectItem OrderSuffix? )* ( 'GROUP' ( 'AND' 'ORDER' )? 'BY' SelectItem OrderSuffix? ( ',' SelectItem OrderSuffix? )* )?
-
- Referenced by: -
- - -====================================================================================================================== -OrderByPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - OrderByElements - -
- - -
         ::= OrderByElements
-
- Referenced by: -
- - -====================================================================================================================== -AsPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - AS - - Alias - -
- - -
         ::= 'AS' Alias
-
- Referenced by: -
- - -====================================================================================================================== -JoinPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - JoinerExpression - -
- - -
         ::= JoinerExpression
-
- Referenced by: -
- - -====================================================================================================================== -SetPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - SET - - UpdateSets - -
- - -
         ::= 'SET' UpdateSets
-
- Referenced by: -
- - -====================================================================================================================== -DropPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - DROP - - ColumnList - -
- - -
         ::= 'DROP' ColumnList
-
- Referenced by: -
- - -====================================================================================================================== -LimitPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - LIMIT - - Expression - OFFSET - - Expression - -
- - -
         ::= 'LIMIT' Expression ( 'OFFSET' Expression )?
-
- Referenced by: -
- - -====================================================================================================================== -SetOperationModifier -====================================================================================================================== - - -.. raw:: html - - - - - - ALL - - DISTINCT - - BY - - NAME - - MATCHING - - ( - - RelObjectName - , - - ) - - STRICT - - CORRESPONDING - - ALL - - DISTINCT - - BY - - ALL - - DISTINCT - - ( - - RelObjectName - , - - ) - - ALL - - DISTINCT - - -
- - -
         ::= ( 'ALL' | 'DISTINCT' )? 'BY' 'NAME' ( 'MATCHING' '(' RelObjectName ( ',' RelObjectName )* ')' )?
-
           | 'STRICT'? 'CORRESPONDING' ( 'ALL' | 'DISTINCT' )? ( 'BY' ( 'ALL' | 'DISTINCT' - )? '(' RelObjectName ( ',' RelObjectName )* ')' )?
-
           | 'ALL'
-
           | 'DISTINCT'
-
- - -====================================================================================================================== -SetOperationPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - UNION - - INTERSECT - - EXCEPT - - SetOperationModifier - - ParenthesedSelect - , - - -
- - -
         ::= ( 'UNION' | 'INTERSECT' | 'EXCEPT' ) SetOperationModifier? ParenthesedSelect ( ',' ParenthesedSelect )*
-
- Referenced by: -
- - -====================================================================================================================== -CallPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - CALL - - TableFunction - - Alias - -
- - -
         ::= 'CALL' TableFunction Alias?
-
- Referenced by: -
- - -====================================================================================================================== -TableSamplePipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - TABLESAMPLE - - SYSTEM - - ( - - S_DOUBLE - - S_LONG - PERCENT - - ) - - -
- - -
         ::= 'TABLESAMPLE' 'SYSTEM' '(' ( S_DOUBLE | S_LONG ) 'PERCENT' ')'
-
- Referenced by: -
- - -====================================================================================================================== -PivotPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - PIVOT - - ( - - Function - FOR - - Column - IN - - ( - - SelectItemsList - ) - - ) - - Alias - -
- - -
         ::= 'PIVOT' '(' Function 'FOR' Column 'IN' '(' SelectItemsList ')' ')' Alias?
-
- Referenced by: -
- - -====================================================================================================================== -UnPivotPipeOperator -====================================================================================================================== - - -.. raw:: html - - - - - - UNPIVOT - - ( - - Column - FOR - - Column - IN - - ( - - SelectItemsList - ) - - ) - - Alias - -
- - -
         ::= 'UNPIVOT' '(' Column 'FOR' Column 'IN' '(' SelectItemsList ')' ')' Alias?
-
- Referenced by: -
- - -====================================================================================================================== -TableStatement -====================================================================================================================== - - -.. raw:: html - - - - - - TABLE - - Table - - OrderByElements - - LimitWithOffset - - Offset - -
- - -
         ::= 'TABLE' Table OrderByElements? LimitWithOffset? Offset?
-
- Referenced by: -
- - -====================================================================================================================== -ParenthesedSelect -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Select - ) - - -
- - -
         ::= '(' Select ')'
-
- - -====================================================================================================================== -ParenthesedInsert -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Insert - ) - - -
- - -
         ::= '(' Insert ')'
-
- Referenced by: -
- - -====================================================================================================================== -ParenthesedUpdate -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Update - ) - - -
- - -
         ::= '(' Update ')'
-
- Referenced by: -
- - -====================================================================================================================== -ParenthesedDelete -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Delete - ) - - -
- - -
         ::= '(' Delete ')'
-
- Referenced by: -
- - -====================================================================================================================== -LateralView -====================================================================================================================== - - -.. raw:: html - - - - - - LATERAL - - VIEW - - OUTER - - Function - - RelObjectName - AS - - RelObjectName - , - - RelObjectName - -
- - -
         ::= 'LATERAL' 'VIEW' 'OUTER'? Function RelObjectName? 'AS' RelObjectName ( ',' RelObjectName )?
-
- Referenced by: -
- - -====================================================================================================================== -ForClause -====================================================================================================================== - - -.. raw:: html - - - - - - FOR - - BROWSE - - XML - - RAW - - ( - - S_CHAR_LITERAL - ) - - AUTO - - , - - BINARY - - BASE64 - - TYPE - - ROOT - - XMLSCHEMA - - ( - - S_CHAR_LITERAL - ) - - XMLDATA - - ELEMENTS - - XSINIL - - ABSENT - - EXPLICIT - - , - - BINARY - - BASE64 - - TYPE - - ROOT - - ( - - S_CHAR_LITERAL - ) - - XMLDATA - - PATH - - ( - - S_CHAR_LITERAL - ) - - , - - BINARY - - BASE64 - - TYPE - - ROOT - - ( - - S_CHAR_LITERAL - ) - - ELEMENTS - - XSINIL - - ABSENT - - JSON - - AUTO - - PATH - - , - - ROOT - - ( - - S_CHAR_LITERAL - ) - - INCLUDE_NULL_VALUES - - WITHOUT_ARRAY_WRAPPER - - -
- - -
         ::= 'FOR' ( 'BROWSE' | 'XML' ( ( 'RAW' ( '(' S_CHAR_LITERAL ')' )? | 'AUTO' ) ( ',' ( 'BINARY' 'BASE64' | 'TYPE' | ( 'ROOT' | 'XMLSCHEMA' ) ( - '(' S_CHAR_LITERAL ')' )? | 'XMLDATA' | 'ELEMENTS' ( 'XSINIL' | 'ABSENT' )? ) )* | 'EXPLICIT' ( ',' - ( 'BINARY' 'BASE64' | 'TYPE' | 'ROOT' ( '(' S_CHAR_LITERAL ')' )? | 'XMLDATA' ) )* | 'PATH' ( '(' S_CHAR_LITERAL ')' )? ( ',' ( 'BINARY' 'BASE64' | 'TYPE' | 'ROOT' ( '(' S_CHAR_LITERAL ')' )? | 'ELEMENTS' ( 'XSINIL' | 'ABSENT' )? ) )* ) | 'JSON' ( 'AUTO' | 'PATH' ) - ( ',' ( 'ROOT' ( '(' S_CHAR_LITERAL ')' )? | 'INCLUDE_NULL_VALUES' | 'WITHOUT_ARRAY_WRAPPER' ) )* )
-
- Referenced by: -
- - -====================================================================================================================== -LateralViews -====================================================================================================================== - - -.. raw:: html - - - - - - LateralView - -
- - -
         ::= LateralView+
-
- Referenced by: -
- - -====================================================================================================================== -LateralSubSelect -====================================================================================================================== - - -.. raw:: html - - - - - - LATERAL - - ( - - Select - ) - - -
- - -
         ::= 'LATERAL' '(' Select ')'
-
- Referenced by: -
- - -====================================================================================================================== -PlainSelect -====================================================================================================================== - - -.. raw:: html - - - - - - K_SELECT - STRAIGHT_JOIN - - Skip - - First - - Top - ALL - - DISTINCT - - ON - - ( - - SelectItemsList - ) - - DISTINCTROW - - UNIQUE - - SQL_CALC_FOUND_ROWS - - SQL_NO_CACHE - - SQL_CACHE - - AS - - STRUCT - - VALUE - - Top - - SelectItemsList - - MySqlSelectIntoClause - - IntoClause - FROM - - FromItem - - LateralViews - - JoinsList - FROM - - ONLY - - FromItem - - LateralViews - - JoinsList - FINAL - - KSQLWindowClause - - PreWhereClause - - WhereClause - - OracleHierarchicalQueryClause - - PreferringClause - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - Having - - GroupByColumnReferences - - Having - - Qualify - - OrderByElements - WINDOW - - RelObjectName - AS - - windowDefinition - , - - OrderByElements - - ForClause - EMIT - - CHANGES - - LimitBy - - LimitWithOffset - - Offset - - LimitWithOffset - - Fetch - - WithIsolation - FOR - - NO - - KEY - - UPDATE - - KEY - - SHARE - - READ - - FETCH - - ONLY - - OF - - Table - - Wait - NOWAIT - - SKIP - - LOCKED - - MySqlSelectIntoClause - SETTINGS - - UpdateSets - - OptimizeFor - INTO - - TEMP - - Table - WITH - - NO - - LOG - - -
- - -
         ::= K_SELECT 'STRAIGHT_JOIN'? Skip? First? Top? ( 'ALL' | 'DISTINCT' ( 'ON' '(' SelectItemsList ')' )? | 'DISTINCTROW' | 'UNIQUE' | 'SQL_CALC_FOUND_ROWS' | 'SQL_NO_CACHE' | 'SQL_CACHE' - )? ( 'AS' ( 'STRUCT' | 'VALUE' ) )? Top? SelectItemsList MySqlSelectIntoClause? IntoClause? ( 'FROM' FromItem LateralViews? JoinsList? )? ( 'FROM' 'ONLY' FromItem LateralViews? JoinsList? )? 'FINAL'? KSQLWindowClause? PreWhereClause? WhereClause? OracleHierarchicalQueryClause? ( PreferringClause ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? )? Having? GroupByColumnReferences? Having? Qualify? OrderByElements? ( 'WINDOW' RelObjectName 'AS' windowDefinition ( ',' RelObjectName 'AS' windowDefinition )* )? OrderByElements? ForClause? ( 'EMIT' 'CHANGES' )? LimitBy? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation? ( 'FOR' ( ( 'NO' 'KEY' )? 'UPDATE' | 'KEY'? 'SHARE' | ( 'READ' | 'FETCH' ) 'ONLY' - ) ( 'OF' Table )? Wait? ( 'NOWAIT' | 'SKIP' 'LOCKED' )? )? MySqlSelectIntoClause? ( 'SETTINGS' UpdateSets )? OptimizeFor? ( 'INTO' 'TEMP' Table )? ( 'WITH' 'NO' 'LOG' )?
-
- Referenced by: -
- - -====================================================================================================================== -SetOperationList -====================================================================================================================== - - -.. raw:: html - - - - - - UNION - - INTERSECT - - MINUS - - EXCEPT - - SetOperationModifier - - PlainSelect - - Values - - ParenthesedSelect - - OrderByElements - - LimitWithOffset - - Offset - - LimitWithOffset - - Fetch - - WithIsolation - -
- - -
         ::= ( ( 'UNION' | 'INTERSECT' | 'MINUS' | 'EXCEPT' ) SetOperationModifier? ( PlainSelect | Values | ParenthesedSelect ) )+ OrderByElements? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation?
-
- Referenced by: -
- - -====================================================================================================================== -WithList -====================================================================================================================== - - -.. raw:: html - - - - - - WITH - - WithItem - , - - -
- -
WithList ::= 'WITH' WithItem ( ',' WithItem )*
-
- Referenced by: -
- - -====================================================================================================================== -WithItem -====================================================================================================================== - - -.. raw:: html - - - - - - FUNCTION - - WithFunctionDeclaration - RECURSIVE - - RelObjectName - ( - - SelectItemsList - ) - - AS - - NOT - - MATERIALIZED - - ParenthesedSelect - - ParenthesedInsert - - ParenthesedUpdate - - ParenthesedDelete - - WithSearchClause - -
- -
WithItem ::= ( 'FUNCTION' WithFunctionDeclaration | 'RECURSIVE'? RelObjectName ( '(' SelectItemsList ')' )? 'AS' ( 'NOT'? 'MATERIALIZED' )? ( ParenthesedSelect | ParenthesedInsert | ParenthesedUpdate | ParenthesedDelete ) ) WithSearchClause?
-
- Referenced by: -
- - -====================================================================================================================== -WithSearchClause -====================================================================================================================== - - -.. raw:: html - - - - - - SEARCH - - BREADTH - - DEPTH - - FIRST - - BY - - Column - , - - SET - - RelObjectName - -
- - -
         ::= 'SEARCH' ( 'BREADTH' | 'DEPTH' ) 'FIRST' 'BY' Column ( ',' Column )* 'SET' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -WithFunctionDeclaration -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ( - - WithFunctionParameter - , - - ) - - RETURNS - - RelObjectName - RETURN - - Expression - -
- - -
         ::= RelObjectName '(' ( WithFunctionParameter ( ',' WithFunctionParameter )* )? ')' 'RETURNS' RelObjectName 'RETURN' Expression
-
- Referenced by: -
- - -====================================================================================================================== -WithFunctionParameter -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ARRAY - - < - - RelObjectName - > - - RelObjectName - -
- - -
         ::= RelObjectName ( 'ARRAY' '<' RelObjectName '>' | RelObjectName )
-
- Referenced by: -
- - -====================================================================================================================== -ColumnSelectItemsList -====================================================================================================================== - - -.. raw:: html - - - - - - SelectItem - , - - -
- - -
         ::= SelectItem ( ',' SelectItem )*
-
- Referenced by: -
- - -====================================================================================================================== -SelectItemsList -====================================================================================================================== - - -.. raw:: html - - - - - - SelectItem - , - - -
- - -
         ::= SelectItem ( ',' SelectItem )*
-
- - -====================================================================================================================== -FunctionAllColumns -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Function - ) - - . - - * - - -
- - -
         ::= '('+ Function ')'+ '.' '*'
-
- Not referenced by any. -
- - -====================================================================================================================== -SelectItem -====================================================================================================================== - - -.. raw:: html - - - - - - ConnectByPriorOperator - - Condition - - Alias - -
- - -
         ::= ( ConnectByPriorOperator | Condition ) Alias?
-
- - -====================================================================================================================== -AllColumns -====================================================================================================================== - - -.. raw:: html - - - - - - * - - EXCEPT - - EXCLUDE - - ParenthesedColumnList - REPLACE - - ( - - SelectItemsList - ) - - -
- - -
         ::= '*' ( ( 'EXCEPT' | 'EXCLUDE' ) ParenthesedColumnList )? ( 'REPLACE' '(' SelectItemsList ')' )?
-
- - -====================================================================================================================== -AllTableColumns -====================================================================================================================== - - -.. raw:: html - - - - - - Table - . - - AllColumns - -
- - -
         ::= Table '.' AllColumns
-
- - -====================================================================================================================== -Alias -====================================================================================================================== - - -.. raw:: html - - - - - - AS - - RelObjectName - ( - - RelObjectName - - ColDataType - , - - ) - - AS - - RelObjectName - - S_CHAR_LITERAL - ( - - RelObjectName - - ColDataType - , - - ) - - -
- - -
           | 'AS'? ( RelObjectName | S_CHAR_LITERAL ) ( '(' RelObjectName ColDataType? ( ',' RelObjectName ColDataType? )* ')' )?
-
- - -====================================================================================================================== -SQLServerHint -====================================================================================================================== - - -.. raw:: html - - - - - - INDEX - - ( - - RelObjectName - ) - - NOLOCK - - -
- - -
         ::= 'INDEX' '(' RelObjectName ')'
-
           | 'NOLOCK'
-
- Referenced by: -
- - -====================================================================================================================== -SQLServerHints -====================================================================================================================== - - -.. raw:: html - - - - - - WITH - - ( - - SQLServerHint - , - - ) - - -
- - -
         ::= 'WITH' '(' SQLServerHint ( ',' SQLServerHint )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -MySQLIndexHint -====================================================================================================================== - - -.. raw:: html - - - - - - USE - - SHOW - - IGNORE - - FORCE - - INDEX - - KEY - - ( - - RelObjectName - , - - ) - - -
- - -
         ::= ( 'USE' | 'SHOW' | 'IGNORE' | 'FORCE' ) ( 'INDEX' | 'KEY' ) '(' RelObjectName ( ',' RelObjectName )* ')'
-
- - -====================================================================================================================== -FunctionItem -====================================================================================================================== - - -.. raw:: html - - - - - - Function - - Alias - -
- - -
         ::= Function Alias?
-
- Referenced by: -
- - -====================================================================================================================== -PivotForColumns -====================================================================================================================== - - -.. raw:: html - - - - - - ParenthesedColumnList - - Column - -
- - -
         ::= ParenthesedColumnList
-
           | Column
-
- Referenced by: -
- - -====================================================================================================================== -PivotFunctionItems -====================================================================================================================== - - -.. raw:: html - - - - - - FunctionItem - , - - -
- - -
         ::= FunctionItem ( ',' FunctionItem )*
-
- Referenced by: -
- - -====================================================================================================================== -ExpressionListItem -====================================================================================================================== - - -.. raw:: html - - - - - - ParenthesedExpressionList - - Alias - -
- - -
         ::= ParenthesedExpressionList Alias?
-
- Referenced by: -
- - -====================================================================================================================== -PivotMultiInItems -====================================================================================================================== - - -.. raw:: html - - - - - - ExpressionListItem - , - - -
- - -
         ::= ExpressionListItem ( ',' ExpressionListItem )*
-
- Referenced by: -
- - -====================================================================================================================== -Pivot -====================================================================================================================== - - -.. raw:: html - - - - - - PIVOT - - ( - - PivotFunctionItems - FOR - - PivotForColumns - IN - - ( - - SelectItemsList - - PivotMultiInItems - ) - - ) - - Alias - -
- -
Pivot    ::= 'PIVOT' '(' PivotFunctionItems 'FOR' PivotForColumns 'IN' '(' ( SelectItemsList | PivotMultiInItems ) ')' ')' Alias?
-
- Referenced by: -
- - -====================================================================================================================== -PivotXml -====================================================================================================================== - - -.. raw:: html - - - - - - PIVOT - - XML - - ( - - PivotFunctionItems - FOR - - PivotForColumns - IN - - ( - - ANY - - Select - - SelectItemsList - - PivotMultiInItems - ) - - ) - - -
- -
PivotXml ::= 'PIVOT' 'XML' '(' PivotFunctionItems 'FOR' PivotForColumns 'IN' '(' ( 'ANY' | Select | SelectItemsList | PivotMultiInItems ) ')' ')'
-
- Referenced by: -
- - -====================================================================================================================== -UnPivot -====================================================================================================================== - - -.. raw:: html - - - - - - UNPIVOT - - INCLUDE - - EXCLUDE - - NULLS - - ( - - PivotForColumns - FOR - - PivotForColumns - IN - - ( - - SelectItemsList - ) - - ) - - Alias - -
- -
UnPivot  ::= 'UNPIVOT' ( ( 'INCLUDE' | 'EXCLUDE' ) 'NULLS' )? '(' PivotForColumns 'FOR' PivotForColumns 'IN' '(' SelectItemsList ')' ')' Alias?
-
- Referenced by: -
- - -====================================================================================================================== -IntoClause -====================================================================================================================== - - -.. raw:: html - - - - - - INTO - - Table - , - - -
- - -
         ::= 'INTO' Table ( ',' Table )*
-
- Referenced by: -
- - -====================================================================================================================== -MySqlSelectIntoClause -====================================================================================================================== - - -.. raw:: html - - - - - - INTO - - OUTFILE - - S_CHAR_LITERAL - - MySqlSelectIntoOutfileTail - DUMPFILE - - S_CHAR_LITERAL - -
- - -
         ::= 'INTO' ( 'OUTFILE' S_CHAR_LITERAL MySqlSelectIntoOutfileTail | 'DUMPFILE' S_CHAR_LITERAL )
-
- Referenced by: -
- - -====================================================================================================================== -MySqlSelectIntoOutfileTail -====================================================================================================================== - - -.. raw:: html - - - - - - CHARACTER - - SET - - S_IDENTIFIER - BINARY - - MySqlSelectIntoFieldsClause - - MySqlSelectIntoLinesClause - -
- - -
         ::= ( 'CHARACTER' 'SET' ( S_IDENTIFIER | 'BINARY' ) )? MySqlSelectIntoFieldsClause? MySqlSelectIntoLinesClause?
-
- Referenced by: -
- - -====================================================================================================================== -MySqlSelectIntoFieldsClause -====================================================================================================================== - - -.. raw:: html - - - - - - FIELDS - - COLUMNS - - TERMINATED - - BY - - S_CHAR_LITERAL - OPTIONALLY - - ENCLOSED - - BY - - S_CHAR_LITERAL - ESCAPED - - BY - - S_CHAR_LITERAL - -
- - -
         ::= ( 'FIELDS' | 'COLUMNS' ) ( 'TERMINATED' 'BY' S_CHAR_LITERAL )? ( 'OPTIONALLY'? 'ENCLOSED' 'BY' S_CHAR_LITERAL )? ( 'ESCAPED' 'BY' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -MySqlSelectIntoLinesClause -====================================================================================================================== - - -.. raw:: html - - - - - - LINES - - STARTING - - BY - - S_CHAR_LITERAL - TERMINATED - - BY - - S_CHAR_LITERAL - -
- - -
         ::= 'LINES' ( 'STARTING' 'BY' S_CHAR_LITERAL )? ( 'TERMINATED' 'BY' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -ParenthesedFromItem -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - FromItem - - JoinsList - ) - - -
- - -
         ::= '(' FromItem JoinsList? ')'
-
- Referenced by: -
- - -====================================================================================================================== -FromItem -====================================================================================================================== - - -.. raw:: html - - - - - - Values - - TableFunction - - Table - - ParenthesedSelect - - Pivot - - UnPivot - - ParenthesedFromItem - - LateralSubSelect - - SubImport - - Select - - Alias - - TimeTravelAfterAlias - - SampleClause - - UnPivot - - PivotXml - - Pivot - - MySQLIndexHint - - SQLServerHints - -
- - -
- - -====================================================================================================================== -JoinsList -====================================================================================================================== - - -.. raw:: html - - - - - - JoinerExpression - -
- - -
         ::= JoinerExpression+
-
- - -====================================================================================================================== -JoinHint -====================================================================================================================== - - -.. raw:: html - - - - - - LOOP - - HASH - - MERGE - - REMOTE - - -
- -
JoinHint ::= 'LOOP'
-
           | 'HASH'
-
           | 'MERGE'
-
           | 'REMOTE'
-
- Referenced by: -
- - -====================================================================================================================== -JoinerExpression -====================================================================================================================== - - -.. raw:: html - - - - - - GLOBAL - - ANY - - ALL - - NATURAL - - LEFT - - SEMI - - OUTER - - ANY - - ALL - - RIGHT - - FULL - - OUTER - - ANY - - ALL - - INNER - - CROSS - - OUTER - - JoinHint - JOIN - - FETCH - - , - - OUTER - - STRAIGHT_JOIN - - APPLY - - FromItem - WITHIN - - ( - - JoinWindow - ) - - ON - - Expression - USING - - ( - - Column - , - - ) - - -
- - -
         ::= 'GLOBAL'? ( 'ANY' | 'ALL' )? 'NATURAL'? ( 'LEFT' ( 'SEMI' | 'OUTER' | - 'ANY' | 'ALL' )? | ( 'RIGHT' | 'FULL' ) ( 'OUTER' | 'ANY' | 'ALL' )? | 'INNER' | 'CROSS' - | 'OUTER' )? ( JoinHint? 'JOIN' 'FETCH'? | ',' 'OUTER'? | 'STRAIGHT_JOIN' | 'APPLY' ) FromItem ( ( 'WITHIN' '(' JoinWindow ')' )? ( 'ON' Expression )+ | 'USING' '(' Column ( ',' Column )* ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -JoinWindow -====================================================================================================================== - - -.. raw:: html - - - - - - S_LONG - - S_IDENTIFIER - - K_DATE_LITERAL - , - - S_LONG - - S_IDENTIFIER - - K_DATE_LITERAL - -
- - -
         ::= S_LONG ( S_IDENTIFIER | K_DATE_LITERAL ) ( ',' S_LONG ( S_IDENTIFIER | K_DATE_LITERAL ) )?
-
- Referenced by: -
- - -====================================================================================================================== -KSQLWindowClause -====================================================================================================================== - - -.. raw:: html - - - - - - WINDOW - - HOPPING - - ( - - SIZE - - S_LONG - - S_IDENTIFIER - , - - ADVANCE - - BY - - SESSION - - ( - - TUMBLING - - ( - - SIZE - - S_LONG - - S_IDENTIFIER - ) - - -
- - -
         ::= 'WINDOW' ( 'HOPPING' '(' 'SIZE' S_LONG S_IDENTIFIER ',' 'ADVANCE' 'BY' | 'SESSION' '(' | 'TUMBLING' '(' 'SIZE' ) S_LONG S_IDENTIFIER ')'
-
- Referenced by: -
- - -====================================================================================================================== -WhereClause -====================================================================================================================== - - -.. raw:: html - - - - - - WHERE - - Expression - -
- - -
         ::= 'WHERE' Expression
-
- - -====================================================================================================================== -PreWhereClause -====================================================================================================================== - - -.. raw:: html - - - - - - PREWHERE - - Expression - -
- - -
         ::= 'PREWHERE' Expression
-
- Referenced by: -
- - -====================================================================================================================== -OracleHierarchicalQueryClause -====================================================================================================================== - - -.. raw:: html - - - - - - START - - WITH - - XorExpression - CONNECT - - BY - - NOCYCLE - - CONNECT - - BY - - NOCYCLE - - XorExpression - START - - WITH - - XorExpression - -
- - -
         ::= ( 'START' 'WITH' XorExpression 'CONNECT' 'BY' 'NOCYCLE'? | 'CONNECT' 'BY' 'NOCYCLE'? ( XorExpression 'START' 'WITH' )? ) XorExpression
-
- Referenced by: -
- - -====================================================================================================================== -PreferringClause -====================================================================================================================== - - -.. raw:: html - - - - - - PREFERRING - - PreferenceTerm - -
- - -
         ::= 'PREFERRING' PreferenceTerm
-
- Referenced by: -
- - -====================================================================================================================== -PreferenceTerm -====================================================================================================================== - - -.. raw:: html - - - - - - Plus - -
- - -
         ::= Plus
-
- Referenced by: -
- - -====================================================================================================================== -Plus -====================================================================================================================== - - -.. raw:: html - - - - - - PriorTo - PLUS - - -
- -
Plus     ::= PriorTo ( 'PLUS' PriorTo )*
-
- Referenced by: -
- - -====================================================================================================================== -PriorTo -====================================================================================================================== - - -.. raw:: html - - - - - - PreferenceTermTerminal - ( - - PreferenceTerm - ) - - TO - - PRIOR - - -
- -
PriorTo  ::= ( PreferenceTermTerminal | '(' PreferenceTerm ')' ) ( 'PRIOR' 'TO' ( PreferenceTermTerminal | '(' PreferenceTerm ')' ) )*
-
- Referenced by: -
- - -====================================================================================================================== -PreferenceTermTerminal -====================================================================================================================== - - -.. raw:: html - - - - - - HighExpression - - LowExpression - - Inverse - - Condition - -
- - -
         ::= HighExpression
-
           | LowExpression
-
           | Inverse
-
           | Condition
-
- Referenced by: -
- - -====================================================================================================================== -HighExpression -====================================================================================================================== - - -.. raw:: html - - - - - - HIGH - - Expression - -
- - -
         ::= 'HIGH' Expression
-
- Referenced by: -
- - -====================================================================================================================== -LowExpression -====================================================================================================================== - - -.. raw:: html - - - - - - LOW - - Expression - -
- - -
         ::= 'LOW' Expression
-
- Referenced by: -
- - -====================================================================================================================== -Inverse -====================================================================================================================== - - -.. raw:: html - - - - - - INVERSE - - ( - - PreferenceTerm - ) - - -
- -
Inverse  ::= 'INVERSE' '(' PreferenceTerm ')'
-
- Referenced by: -
- - -====================================================================================================================== -GroupByColumnReferences -====================================================================================================================== - - -.. raw:: html - - - - - - GROUP - - BY - - GROUPING - - SETS - - ( - - GroupingSet - , - - ) - - ExpressionList - GROUPING - - SETS - - ( - - GroupingSet - , - - ) - - WITH - - ROLLUP - - -
- - -
         ::= 'GROUP' 'BY' ( 'GROUPING' 'SETS' '(' GroupingSet ( ',' GroupingSet )* ')' | ExpressionList ( 'GROUPING' 'SETS' '(' GroupingSet ( ',' GroupingSet )* ')' )? ( 'WITH' 'ROLLUP' )? )
-
- Referenced by: -
- - -====================================================================================================================== -GroupingSet -====================================================================================================================== - - -.. raw:: html - - - - - - ParenthesedExpressionList - - SimpleExpression - -
- - -
         ::= ParenthesedExpressionList
-
           | SimpleExpression
-
- Referenced by: -
- - -====================================================================================================================== -Having -====================================================================================================================== - - -.. raw:: html - - - - - - HAVING - - Expression - -
- -
Having   ::= 'HAVING' Expression
-
- Referenced by: -
- - -====================================================================================================================== -Qualify -====================================================================================================================== - - -.. raw:: html - - - - - - QUALIFY - - Expression - -
- -
Qualify  ::= 'QUALIFY' Expression
-
- Referenced by: -
- - -====================================================================================================================== -OrderByElements -====================================================================================================================== - - -.. raw:: html - - - - - - ORDER - - SIBLINGS - - BY - - OrderByElement - , - - -
- - -
         ::= 'ORDER' 'SIBLINGS'? 'BY' OrderByElement ( ',' OrderByElement )*
-
- - -====================================================================================================================== -OrderByElement -====================================================================================================================== - - -.. raw:: html - - - - - - Expression - COLLATE - - S_CHAR_LITERAL - - S_QUOTED_IDENTIFIER - ASC - - DESC - - NULLS - - FIRST - - LAST - - WITH - - ROLLUP - - -
- - -
         ::= Expression ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER ) )? ( 'ASC' | 'DESC' )? ( 'NULLS' ( 'FIRST' | 'LAST' )? )? ( 'WITH' 'ROLLUP' )?
-
- Referenced by: -
- - -====================================================================================================================== -JdbcParameter -====================================================================================================================== - - -.. raw:: html - - - - - - ? - - S_PARAMETER - - S_LONG - -
- - -
         ::= ( '?' | S_PARAMETER ) S_LONG?
-
- Referenced by: -
- - -====================================================================================================================== -LimitWithOffset -====================================================================================================================== - - -.. raw:: html - - - - - - LIMIT - - ParenthesedSelect - - Expression - , - - Expression - -
- - -
         ::= 'LIMIT' ( ParenthesedSelect | Expression ) ( ',' Expression )?
-
- - -====================================================================================================================== -PlainLimit -====================================================================================================================== - - -.. raw:: html - - - - - - LIMIT - - ParenthesedSelect - - Expression - -
- - -
         ::= 'LIMIT' ( ParenthesedSelect | Expression )
-
- Referenced by: -
- - -====================================================================================================================== -LimitBy -====================================================================================================================== - - -.. raw:: html - - - - - - LimitWithOffset - BY - - ExpressionList - -
- - -
- Referenced by: -
- - -====================================================================================================================== -Offset -====================================================================================================================== - - -.. raw:: html - - - - - - OFFSET - - Expression - ROWS - - ROW - - -
- -
Offset   ::= 'OFFSET' Expression ( 'ROWS' | 'ROW' )?
-
- - -====================================================================================================================== -Fetch -====================================================================================================================== - - -.. raw:: html - - - - - - FETCH - - FIRST - - NEXT - - Expression - PERCENT - - ROWS - - ROW - - ONLY - - WITH TIES - - -
- -
Fetch    ::= 'FETCH' ( 'FIRST' | 'NEXT' ) ( Expression 'PERCENT'? )? ( 'ROWS' | 'ROW' ) ( 'ONLY' | 'WITH TIES' )
-
- - -====================================================================================================================== -WithIsolation -====================================================================================================================== - - -.. raw:: html - - - - - - WITH - - K_ISOLATION - -
- - -
         ::= 'WITH' K_ISOLATION
-
- - -====================================================================================================================== -OptimizeFor -====================================================================================================================== - - -.. raw:: html - - - - - - OPTIMIZE - - FOR - - S_LONG - ROWS - - -
- - -
         ::= 'OPTIMIZE' 'FOR' S_LONG 'ROWS'
-
- Referenced by: -
- - -====================================================================================================================== -Top -====================================================================================================================== - - -.. raw:: html - - - - - - TOP - - S_LONG - - JdbcParameter - : - - S_IDENTIFIER - ( - - SimpleExpression - ) - - PERCENT - - WITH TIES - - -
- -
Top      ::= 'TOP' ( S_LONG | JdbcParameter | ':' S_IDENTIFIER? | '(' SimpleExpression ')' ) 'PERCENT'? 'WITH TIES'?
-
- Referenced by: -
- - -====================================================================================================================== -Skip -====================================================================================================================== - - -.. raw:: html - - - - - - SKIP - - S_LONG - - S_IDENTIFIER - - JdbcParameter - -
- -
Skip     ::= 'SKIP' ( S_LONG | S_IDENTIFIER | JdbcParameter )
-
- Referenced by: -
- - -====================================================================================================================== -First -====================================================================================================================== - - -.. raw:: html - - - - - - FIRST - - LIMIT - - S_LONG - - S_IDENTIFIER - - JdbcParameter - -
- -
First    ::= ( 'FIRST' | 'LIMIT' ) ( S_LONG | S_IDENTIFIER | JdbcParameter )
-
- Referenced by: -
- - -====================================================================================================================== -Expression -====================================================================================================================== - - -.. raw:: html - - - - - - Condition - -
- - -
         ::= Condition
-
- - -====================================================================================================================== -XorExpression -====================================================================================================================== - - -.. raw:: html - - - - - - AndChain - OR - - XOR - - -
- - -
         ::= AndChain ( ( 'OR' | 'XOR' ) AndChain )*
-
- Referenced by: -
- - -====================================================================================================================== -AndChain -====================================================================================================================== - - -.. raw:: html - - - - - - Condition - AND - - && - - -
- -
AndChain ::= Condition ( ( 'AND' | '&&' ) Condition )*
-
- Referenced by: -
- - -====================================================================================================================== -Condition -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - ! - - ExistsExpression - PRIOR - - SimpleExpression - ( - - + - - ) - - RegularConditionRHS - - OverlapsCondition - - InExpression - - ExcludesExpression - - IncludesExpression - - Between - - MemberOfExpression - - IsNullExpression - - IsBooleanExpression - - IsUnknownExpression - - LikeExpression - - IsDistinctExpression - - SimilarToExpression - -
- - - -
- - -====================================================================================================================== -RegularConditionRHS -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - + - - ) - - > - - < - - = - - OP_GREATERTHANEQUALS - - OP_MINORTHANEQUALS - - OP_NOTEQUALSSTANDARD - - OP_NOTEQUALSBANG - ^= - - *= - - =* - - && - - &> - - <& - - @@ - - ~ - - ~* - - !~ - - !~* - - @> - - <@ - - ? - - ?| - - ?& - - || - - - - - -# - - <-> - - <#> - - <=> - - PRIOR - - ComparisonItem - ( - - + - - ) - - -
- - -
         ::= ( '(' '+' ')' )? ( '>' | '<' | '=' | OP_GREATERTHANEQUALS | OP_MINORTHANEQUALS | OP_NOTEQUALSSTANDARD | OP_NOTEQUALSBANG | '^=' | '*=' | '=*' | '&&' | '&>' | '<&' | '@@' | '~' | '~*' | '!~' | '!~*' | '@>' - | '<@' | '?' | '?|' | '?&' | '||' | '-' | '-#' | '<->' | '<#>' | '<=>' ) 'PRIOR'? - ComparisonItem ( '(' '+' ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -OverlapsCondition -====================================================================================================================== - - -.. raw:: html - - - - - - OVERLAPS - - ParenthesedExpressionList - -
- - -
         ::= 'OVERLAPS' ParenthesedExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -SQLCondition -====================================================================================================================== - - -.. raw:: html - - - - - - ExistsExpression - - SimpleExpression - - OverlapsCondition - - InExpression - - ExcludesExpression - - IncludesExpression - - Between - - MemberOfExpression - - IsNullExpression - - IsBooleanExpression - - IsUnknownExpression - - LikeExpression - - IsDistinctExpression - - SimilarToExpression - -
- - -
         ::= ExistsExpression
-
-
- Not referenced by any. -
- - -====================================================================================================================== -InExpression -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - + - - ) - - GLOBAL - - NOT - - IN - - S_CHAR_LITERAL - - ParenthesedSelect - - PrimaryExpression - -
- - -
         ::= ( '(' '+' ')' )? 'GLOBAL'? 'NOT'? 'IN' ( S_CHAR_LITERAL | ParenthesedSelect | PrimaryExpression )
-
- Referenced by: -
- - -====================================================================================================================== -IncludesExpression -====================================================================================================================== - - -.. raw:: html - - - - - - INCLUDES - - ParenthesedExpressionList - -
- - -
         ::= 'INCLUDES' ParenthesedExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -ExcludesExpression -====================================================================================================================== - - -.. raw:: html - - - - - - EXCLUDES - - ParenthesedExpressionList - -
- - -
         ::= 'EXCLUDES' ParenthesedExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -Between -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - BETWEEN - - SYMMETRIC - - ASYMMETRIC - - ParenthesedSelect - - SimpleExpression - - RegularConditionRHS - AND - - ParenthesedSelect - - SimpleExpression - - RegularConditionRHS - -
- -
Between  ::= 'NOT'? 'BETWEEN' ( 'SYMMETRIC' | 'ASYMMETRIC' )? ( ParenthesedSelect | SimpleExpression RegularConditionRHS? ) 'AND' ( ParenthesedSelect | SimpleExpression RegularConditionRHS? )
-
- Referenced by: -
- - -====================================================================================================================== -LikeExpression -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - LIKE - - ILIKE - - RLIKE - - REGEXP_LIKE - - REGEXP - - K_SIMILAR_TO - MATCH_ANY - - MATCH_ALL - - MATCH_PHRASE - - MATCH_PHRASE_PREFIX - - MATCH_REGEXP - - BINARY - - SimpleExpression - ESCAPE - - S_CHAR_LITERAL - - Expression - -
- - -
         ::= 'NOT'? ( 'LIKE' | 'ILIKE' | 'RLIKE' | 'REGEXP_LIKE' | 'REGEXP' | K_SIMILAR_TO | 'MATCH_ANY' | 'MATCH_ALL' | 'MATCH_PHRASE' | 'MATCH_PHRASE_PREFIX' | 'MATCH_REGEXP' - ) 'BINARY'? SimpleExpression ( 'ESCAPE' ( S_CHAR_LITERAL | Expression ) )?
-
- Referenced by: -
- - -====================================================================================================================== -SimilarToExpression -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - SIMILAR - - TO - - SimpleExpression - ESCAPE - - S_CHAR_LITERAL - -
- - -
         ::= 'NOT'? 'SIMILAR' 'TO' SimpleExpression ( 'ESCAPE' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -IsDistinctExpression -====================================================================================================================== - - -.. raw:: html - - - - - - IS - - NOT - - DISTINCT - - FROM - - SimpleExpression - -
- - -
         ::= 'IS' 'NOT'? 'DISTINCT' 'FROM' SimpleExpression
-
- Referenced by: -
- - -====================================================================================================================== -IsNullExpression -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - ISNULL - - NOTNULL - - IS - - NOT - - NULL - - -
- - -
         ::= 'NOT'? 'ISNULL'
-
           | 'NOTNULL'
-
           | 'IS' 'NOT'? 'NULL'
-
- Referenced by: -
- - -====================================================================================================================== -IsBooleanExpression -====================================================================================================================== - - -.. raw:: html - - - - - - IS - - NOT - - TRUE - - FALSE - - -
- - -
         ::= 'IS' 'NOT'? ( 'TRUE' | 'FALSE' )
-
- Referenced by: -
- - -====================================================================================================================== -IsUnknownExpression -====================================================================================================================== - - -.. raw:: html - - - - - - IS - - NOT - - UNKNOWN - - -
- - -
         ::= 'IS' 'NOT'? 'UNKNOWN'
-
- Referenced by: -
- - -====================================================================================================================== -ExistsExpression -====================================================================================================================== - - -.. raw:: html - - - - - - EXISTS - - SimpleExpression - -
- - -
         ::= 'EXISTS' SimpleExpression
-
- Referenced by: -
- - -====================================================================================================================== -MemberOfExpression -====================================================================================================================== - - -.. raw:: html - - - - - - MEMBER - - OF - - Expression - -
- - -
         ::= 'MEMBER' 'OF' Expression
-
- Referenced by: -
- - -====================================================================================================================== -ExpressionList -====================================================================================================================== - - -.. raw:: html - - - - - - ComplexExpressionList - - SimpleExpressionList - - ParenthesedExpressionList - -
- - -
         ::= ComplexExpressionList
-
           | SimpleExpressionList
-
           | ParenthesedExpressionList
-
- - -====================================================================================================================== -ParenthesedExpressionList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - ComplexExpressionList - - SimpleExpressionList - ) - - -
- - -
         ::= '(' ( ComplexExpressionList | SimpleExpressionList )? ')'
-
- - -====================================================================================================================== -SimpleExpressionList -====================================================================================================================== - - -.. raw:: html - - - - - - SimpleExpression - , - - LambdaExpression - - SimpleExpression - -
- - -
         ::= SimpleExpression ( ',' ( LambdaExpression | SimpleExpression ) )*
-
- - -====================================================================================================================== -ColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - Column - , - - -
- - -
         ::= Column ( ',' Column )*
-
- - -====================================================================================================================== -ParenthesedColumnList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - ColumnList - ) - - -
- - -
         ::= '(' ColumnList ')'
-
- - -====================================================================================================================== -ComplexExpressionList -====================================================================================================================== - - -.. raw:: html - - - - - - OracleNamedFunctionParameter - - PostgresNamedFunctionParameter - - Expression - , - - OracleNamedFunctionParameter - - PostgresNamedFunctionParameter - - LambdaExpression - - Expression - -
- - - -
- - -====================================================================================================================== -NamedExpressionListExprFirst -====================================================================================================================== - - -.. raw:: html - - - - - - SimpleExpression - FROM - - IN - - PLACING - - SimpleExpression - FOR - - FROM - - SimpleExpression - FOR - - SimpleExpression - -
- - -
         ::= SimpleExpression ( 'FROM' | 'IN' | 'PLACING' ) SimpleExpression ( ( 'FOR' | 'FROM' ) SimpleExpression ( 'FOR' SimpleExpression )? )?
-
- - -====================================================================================================================== -ComparisonItem -====================================================================================================================== - - -.. raw:: html - - - - - - AnyComparisonExpression - - RowConstructor - - SimpleExpression - -
- - -
         ::= AnyComparisonExpression
-
           | RowConstructor
-
           | SimpleExpression
-
- Referenced by: -
- - -====================================================================================================================== -AnyComparisonExpression -====================================================================================================================== - - -.. raw:: html - - - - - - ANY - - SOME - - ALL - - ParenthesedSelect - -
- - -
         ::= ( 'ANY' | 'SOME' | 'ALL' ) ParenthesedSelect
-
- Referenced by: -
- - -====================================================================================================================== -SimpleExpression -====================================================================================================================== - - -.. raw:: html - - - - - - UserVariable - = - - := - - PrimaryExpression - -
- - -
         ::= ( UserVariable ( '=' | ':=' ) )? PrimaryExpression
-
- - -====================================================================================================================== -MulChain -====================================================================================================================== - - -.. raw:: html - - - - - - PrimaryExpression - * - - / - - % - - ^ - - DIV - - -
- -
MulChain ::= PrimaryExpression ( ( '*' | '/' | '%' | '^' | 'DIV' ) PrimaryExpression )*
-
- Referenced by: -
- - -====================================================================================================================== -AddChain -====================================================================================================================== - - -.. raw:: html - - - - - - MulChain - + - - - - - || - - & - - << - - >> - - | - - | - - -
- -
AddChain ::= MulChain ( ( '+' | '-' | '||' | '&' | '<<' | '>>' | '|' '|'? ) MulChain )*
-
- Not referenced by any. -
- - -====================================================================================================================== -ArrayExpression -====================================================================================================================== - - -.. raw:: html - - - - - - [ - - SimpleExpression - : - - SimpleExpression - ] - - -
- - -
         ::= ( '[' SimpleExpression? ( ':' SimpleExpression? )? ']' )+
-
- Referenced by: -
- - -====================================================================================================================== -PrimaryExpression -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - ! - - + - - - - - ~ - - NULL - - CaseWhenExpression - - CharacterPrimary - - ImplicitCast - - JdbcParameter - - JdbcNamedParameter - - UserVariable - - NumericBind - - ExtractExpression - - XMLSerializeExpr - - JsonFunction - - JsonAggregateFunction - - FullTextSearch - - CastExpression - - Function - - AnalyticExpression - - DateUnitExpression - - IntervalExpression - - S_DOUBLE - - S_LONG - - S_HEX - - AllColumns - - AllTableColumns - - K_TIME_KEY_EXPR - CURRENT - - DateTimeLiteralExpression - - StructType - ARRAY - - < - - ColDataType - > - - ArrayConstructor - - NextValExpression - - ConnectByRootOperator - - ConnectByPriorOperator - - KeyExpression - ALL - - Column - ( - - + - - ) - - TRUE - - FALSE - - S_CHAR_LITERAL - {d - - {t - - {ts - - S_CHAR_LITERAL - } - - Select - - ParenthesedSelect - - ParenthesedExpressionList - -> - - Expression - . - - RelObjectName - COLLATE - - S_CHAR_LITERAL - - S_QUOTED_IDENTIFIER - - S_IDENTIFIER - - IntervalExpressionWithoutInterval - - ArrayExpression - :: - - ColDataType - -> - - : - - ->> - - #> - - #>> - - Expression - - SimpleExpression - - JsonExpression - AT - - K_DATETIMELITERAL - ZONE - - PrimaryExpression - -
- - -
         ::= ( 'NOT' | '!' )? ( '+' | '-' | '~' )? ( 'NULL' | CaseWhenExpression | CharacterPrimary | ImplicitCast | JdbcParameter | JdbcNamedParameter | UserVariable | NumericBind | ExtractExpression | XMLSerializeExpr | JsonFunction | JsonAggregateFunction | FullTextSearch | CastExpression | Function AnalyticExpression? | DateUnitExpression | IntervalExpression | S_DOUBLE | S_LONG | S_HEX | AllColumns | AllTableColumns | K_TIME_KEY_EXPR | 'CURRENT' | DateTimeLiteralExpression | StructType | ( 'ARRAY' ( '<' ColDataType '>' )? )? ArrayConstructor | NextValExpression | ConnectByRootOperator | ConnectByPriorOperator | KeyExpression | 'ALL' | Column ( '(' '+' ')' )? | 'TRUE' | 'FALSE' | S_CHAR_LITERAL | ( '{d' | '{t' | '{ts' ) S_CHAR_LITERAL '}' | Select | ParenthesedSelect | ParenthesedExpressionList ( '->' Expression )? ( '.' RelObjectName )* ) ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER | S_IDENTIFIER ) )? IntervalExpressionWithoutInterval? ArrayExpression? ( '::' ColDataType )* ( ( ( '->' | ':' | '->>' | '#>' | '#>>' ) ( Expression | SimpleExpression ) )+ JsonExpression )? ( 'AT' K_DATETIMELITERAL 'ZONE' PrimaryExpression )*
-
- - -====================================================================================================================== -ConnectByRootOperator -====================================================================================================================== - - -.. raw:: html - - - - - - CONNECT_BY_ROOT - - Expression - -
- - -
         ::= 'CONNECT_BY_ROOT' Expression
-
- Referenced by: -
- - -====================================================================================================================== -ConnectByPriorOperator -====================================================================================================================== - - -.. raw:: html - - - - - - PRIOR - - Expression - -
- - -
         ::= 'PRIOR' Expression
-
- Referenced by: -
- - -====================================================================================================================== -KeyExpression -====================================================================================================================== - - -.. raw:: html - - - - - - KEY - - Expression - -
- - -
         ::= 'KEY' Expression
-
- Referenced by: -
- - -====================================================================================================================== -NextValExpression -====================================================================================================================== - - -.. raw:: html - - - - - - K_NEXTVAL - - RelObjectNames - -
- - -
         ::= K_NEXTVAL RelObjectNames
-
- Referenced by: -
- - -====================================================================================================================== -JdbcNamedParameter -====================================================================================================================== - - -.. raw:: html - - - - - - : - - & - - IdentifierChain - -
- - -
         ::= ( ':' | '&' ) IdentifierChain
-
- Referenced by: -
- - -====================================================================================================================== -OracleNamedFunctionParameter -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNameExt - OUTER - - => - - Expression - -
- - -
         ::= ( RelObjectNameExt | 'OUTER' ) '=>' Expression
-
- Referenced by: -
- - -====================================================================================================================== -PostgresNamedFunctionParameter -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNameExt - OUTER - - := - - Expression - -
- - -
         ::= ( RelObjectNameExt | 'OUTER' ) ':=' Expression
-
- Referenced by: -
- - -====================================================================================================================== -UserVariable -====================================================================================================================== - - -.. raw:: html - - - - - - S_AT_IDENTIFIER - - IdentifierChain2 - -
- - -
         ::= S_AT_IDENTIFIER IdentifierChain2
-
- - -====================================================================================================================== -NumericBind -====================================================================================================================== - - -.. raw:: html - - - - - - : - - S_LONG - -
- - -
         ::= ':' S_LONG
-
- Referenced by: -
- - -====================================================================================================================== -DateTimeLiteralExpression -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATETIMELITERAL - - S_CHAR_LITERAL - - S_QUOTED_IDENTIFIER - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -DateUnitExpression -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATE_LITERAL - -
- - -
         ::= K_DATE_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -RangeExpression -====================================================================================================================== - - -.. raw:: html - - - - - - : - - Expression - -
- - -
         ::= ':' Expression
-
- Referenced by: -
- - -====================================================================================================================== -ArrayConstructor -====================================================================================================================== - - -.. raw:: html - - - - - - [ - - Expression - - RangeExpression - - ArrayConstructor - , - - ] - - -
- - -
         ::= '[' ( ( Expression RangeExpression? | ArrayConstructor ) ( ',' ( Expression RangeExpression? | ArrayConstructor ) )* )? ']'
-
- - -====================================================================================================================== -StructParameters -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - - ColDataType - , - - -
- - -
         ::= RelObjectName? ColDataType ( ',' RelObjectName? ColDataType )*
-
- Referenced by: -
- - -====================================================================================================================== -StructType -====================================================================================================================== - - -.. raw:: html - - - - - - STRUCT - - < - - StructParameters - > - - ( - - SelectItemsList - ) - - { - - RelObjectNameExt - - S_CHAR_LITERAL - : - - Expression - , - - } - - :: - - STRUCT - - ( - - StructParameters - ) - - -
- - -
         ::= 'STRUCT' ( '<' StructParameters '>' )? '(' SelectItemsList ')'
-
           | '{' ( RelObjectNameExt | S_CHAR_LITERAL ) ':' Expression ( ',' ( RelObjectNameExt | S_CHAR_LITERAL ) ':' Expression )* '}' ( '::' 'STRUCT' '(' StructParameters ')' )*
-
- Referenced by: -
- - -====================================================================================================================== -JsonExpression -====================================================================================================================== - - -.. raw:: html - - - - - - :: - - ColDataType - -> - - : - - ->> - - #> - - #>> - - Expression - - SimpleExpression - -
- - -
         ::= ( ( '::' ColDataType )+ ( ( '->' | ':' | '->>' | '#>' | '#>>' ) ( Expression | SimpleExpression ) )* )*
-
- Referenced by: -
- - -====================================================================================================================== -JsonKeyValuePair -====================================================================================================================== - - -.. raw:: html - - - - - - KEY - - S_CHAR_LITERAL - - Column - - AllTableColumns - - AllColumns - - Expression - VALUE - - : - - , - - Expression - FORMAT - - JSON - - ENCODING - - JsonEncoding - -
- - -
         ::= ( 'KEY'? ( S_CHAR_LITERAL | Column ) | AllTableColumns | AllColumns | Expression ) ( ( 'VALUE' | ':' | ',' ) Expression )? ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )?
-
- Referenced by: -
- - -====================================================================================================================== -JsonObjectBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonKeyValuePair - , - - NULL - - ABSENT - - ON - - NULL - - STRICT - - WITH - - WITHOUT - - UNIQUE - - KEYS - - RETURNING - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - ) - - -
- - -
         ::= '(' ( JsonKeyValuePair ( ',' JsonKeyValuePair )* )? ( ( 'NULL' | 'ABSENT' ) 'ON' 'NULL' )? 'STRICT'? ( ( 'WITH' | 'WITHOUT' ) 'UNIQUE' - 'KEYS' )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonArrayBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - NULL - - ON - - NULL - - Expression - FORMAT - - JSON - - ENCODING - - JsonEncoding - , - - ABSENT - - ON - - NULL - - RETURNING - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - ) - - -
- - -
         ::= '(' ( 'NULL' 'ON' 'NULL' | Expression ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? ( ',' Expression ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )* )* ( 'ABSENT' 'ON' 'NULL' )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonKeyword -====================================================================================================================== - - -.. raw:: html - - - - - - S_IDENTIFIER - -
- - -
         ::= S_IDENTIFIER
-
- - -====================================================================================================================== -JsonEncoding -====================================================================================================================== - - -.. raw:: html - - - - - - S_IDENTIFIER - -
- - -
         ::= S_IDENTIFIER
-
- - -====================================================================================================================== -JsonValueOrQueryInputExpression -====================================================================================================================== - - -.. raw:: html - - - - - - Expression - FORMAT - - JSON - - ENCODING - - JsonEncoding - -
- - -
         ::= Expression ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )?
-
- - -====================================================================================================================== -JsonValueOnResponseBehavior -====================================================================================================================== - - -.. raw:: html - - - - - - ERROR - - NULL - - EMPTY - - DEFAULT - - Expression - -
- - -
         ::= 'ERROR'
-
           | 'NULL'
-
           | 'EMPTY'
-
           | 'DEFAULT' Expression
-
- Referenced by: -
- - -====================================================================================================================== -JsonQueryOnResponseBehavior -====================================================================================================================== - - -.. raw:: html - - - - - - ERROR - - NULL - - TRUE - - FALSE - - EMPTY - - ARRAY - - JsonKeyword - -
- - -
         ::= 'ERROR'
-
           | 'NULL'
-
           | 'TRUE'
-
           | 'FALSE'
-
           | 'EMPTY' ( 'ARRAY' | JsonKeyword )?
-
- - -====================================================================================================================== -JsonExistsOnResponseBehavior -====================================================================================================================== - - -.. raw:: html - - - - - - TRUE - - FALSE - - UNKNOWN - - ERROR - - -
- - -
         ::= 'TRUE'
-
           | 'FALSE'
-
           | 'UNKNOWN'
-
           | 'ERROR'
-
- Referenced by: -
- - -====================================================================================================================== -JsonExistsBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonValueOrQueryInputExpression - , - - Expression - - JsonKeyword - - Expression - , - - JsonExistsOnResponseBehavior - ON - - ERROR - - ) - - -
- - -
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( JsonExistsOnResponseBehavior 'ON' 'ERROR' )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonValueBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonValueOrQueryInputExpression - , - - Expression - - JsonKeyword - - Expression - , - - RETURNING - - ColDataType - - JsonValueOnResponseBehavior - ON - - EMPTY - - JsonValueOnResponseBehavior - ON - - ERROR - - ) - - -
- - -
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType )? ( JsonValueOnResponseBehavior 'ON' 'EMPTY' )? ( JsonValueOnResponseBehavior 'ON' 'ERROR' )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonQueryBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonValueOrQueryInputExpression - , - - Expression - - JsonKeyword - - Expression - , - - RETURNING - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - WITHOUT - - WITH - - S_IDENTIFIER - ARRAY - - JsonKeyword - KEEP - - JsonKeyword - - JsonKeyword - ON - - JsonKeyword - STRING - - JsonQueryOnResponseBehavior - ON - - EMPTY - - JsonQueryOnResponseBehavior - ON - - ERROR - - Expression - , - - ) - - -
- - -
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' 'EMPTY' )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? ( ',' Expression ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' 'EMPTY' )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonFunction -====================================================================================================================== - - -.. raw:: html - - - - - - JSON_OBJECT - - JsonObjectBody - JSON_ARRAY - - JsonArrayBody - - JsonKeyword - - JsonValueBody - - JsonQueryBody - - JsonExistsBody - -
- - -
         ::= 'JSON_OBJECT' JsonObjectBody
-
           | 'JSON_ARRAY' JsonArrayBody
-
           | JsonKeyword ( JsonValueBody | JsonQueryBody | JsonExistsBody )
-
- Referenced by: -
- - -====================================================================================================================== -JsonAggregateFunction -====================================================================================================================== - - -.. raw:: html - - - - - - JSON_OBJECTAGG - - ( - - KEY - - DT_ZONE - - S_DOUBLE - - S_LONG - - S_HEX - - S_CHAR_LITERAL - - Column - : - - , - - VALUE - - Expression - FORMAT - - JSON - - NULL - - ABSENT - - ON - - NULL - - WITH - - WITHOUT - - UNIQUE - - KEYS - - JSON_ARRAYAGG - - ( - - Expression - FORMAT - - JSON - - OrderByElements - NULL - - ABSENT - - ON - - NULL - - ) - - FILTER - - ( - - WHERE - - Expression - ) - - OVER - - ( - - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - OrderByElements - - WindowElement - ) - - -
- - -
         ::= ( 'JSON_OBJECTAGG' '(' 'KEY'? ( DT_ZONE | S_DOUBLE | S_LONG | S_HEX | S_CHAR_LITERAL | Column ) ( ':' | ',' | 'VALUE' ) Expression ( 'FORMAT' 'JSON' )? ( ( 'NULL' | 'ABSENT' ) 'ON' 'NULL' )? ( ( 'WITH' | 'WITHOUT' - ) 'UNIQUE' 'KEYS' )? | 'JSON_ARRAYAGG' '(' Expression ( 'FORMAT' 'JSON' )? OrderByElements? ( ( 'NULL' | 'ABSENT' ) 'ON' 'NULL' )? ) ')' ( 'FILTER' '(' 'WHERE' Expression ')' )? ( 'OVER' '(' ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? OrderByElements? WindowElement? ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -IntervalExpression -====================================================================================================================== - - -.. raw:: html - - - - - - INTERVAL - - - - - S_LONG - - S_DOUBLE - - S_CHAR_LITERAL - - Expression - - S_IDENTIFIER - - K_DATE_LITERAL - -
- - -
         ::= 'INTERVAL' ( '-'? ( S_LONG | S_DOUBLE ) | S_CHAR_LITERAL | Expression ) ( S_IDENTIFIER | K_DATE_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -IntervalExpressionWithoutInterval -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATE_LITERAL - -
- - -
         ::= K_DATE_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -KeepExpression -====================================================================================================================== - - -.. raw:: html - - - - - - KEEP - - ( - - S_IDENTIFIER - FIRST - - LAST - - OrderByElements - ) - - -
- - -
         ::= 'KEEP' '(' S_IDENTIFIER ( 'FIRST' | 'LAST' ) OrderByElements ')'
-
- Referenced by: -
- - -====================================================================================================================== -windowFun -====================================================================================================================== - - -.. raw:: html - - - - - - OVER - - WITHIN - - GROUP - - RelObjectName - - windowDefinition - OVER - - ( - - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - ) - - -
- - -
         ::= ( 'OVER' | 'WITHIN' 'GROUP' ) ( RelObjectName | windowDefinition ( 'OVER' '(' ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? ')' )? )
-
- Referenced by: -
- - -====================================================================================================================== -windowDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - OrderByElements - - WindowElement - ) - - -
- - -
         ::= '(' ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? OrderByElements? WindowElement? ')'
-
- Referenced by: -
- - -====================================================================================================================== -AnalyticExpression -====================================================================================================================== - - -.. raw:: html - - - - - - FILTER - - ( - - WHERE - - Expression - ) - - windowFun - - windowFun - -
- - -
         ::= 'FILTER' '(' 'WHERE' Expression ')' windowFun?
-
           | windowFun
-
- Referenced by: -
- - -====================================================================================================================== -WindowElement -====================================================================================================================== - - -.. raw:: html - - - - - - ROWS - - RANGE - - BETWEEN - - WindowOffset - AND - - WindowOffset - -
- - -
         ::= ( 'ROWS' | 'RANGE' ) ( 'BETWEEN' WindowOffset 'AND' )? WindowOffset
-
- - -====================================================================================================================== -WindowOffset -====================================================================================================================== - - -.. raw:: html - - - - - - UNBOUNDED - - SimpleExpression - PRECEDING - - FOLLOWING - - CURRENT - - ROW - - -
- - -
         ::= ( 'UNBOUNDED' | SimpleExpression ) ( 'PRECEDING' | 'FOLLOWING' )
-
           | 'CURRENT' 'ROW'
-
- Referenced by: -
- - -====================================================================================================================== -ExtractExpression -====================================================================================================================== - - -.. raw:: html - - - - - - EXTRACT - - ( - - RelObjectName - - S_CHAR_LITERAL - FROM - - SimpleExpression - ) - - -
- - -
         ::= 'EXTRACT' '(' ( RelObjectName | S_CHAR_LITERAL ) 'FROM' SimpleExpression ')'
-
- Referenced by: -
- - -====================================================================================================================== -ImplicitCast -====================================================================================================================== - - -.. raw:: html - - - - - - DataType - - S_CHAR_LITERAL - - S_LONG - - S_DOUBLE - -
- - -
         ::= DataType ( S_CHAR_LITERAL | S_LONG | S_DOUBLE )
-
- Referenced by: -
- - -====================================================================================================================== -CastExpression -====================================================================================================================== - - -.. raw:: html - - - - - - CAST - - SAFE_CAST - - TRY_CAST - - INTERPRET - - ( - - SimpleExpression - AS - - ROW - - ( - - ColumnDefinition - , - - ) - - ColDataType - FORMAT - - S_CHAR_LITERAL - ) - - -
- - -
         ::= ( 'CAST' | 'SAFE_CAST' | 'TRY_CAST' | 'INTERPRET' ) '(' SimpleExpression 'AS' ( 'ROW' '(' ColumnDefinition ( ',' ColumnDefinition )* ')' | ColDataType ) ( 'FORMAT' S_CHAR_LITERAL )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -CaseWhenExpression -====================================================================================================================== - - -.. raw:: html - - - - - - CASE - - Expression - - WhenThenSearchCondition - ELSE - - Expression - - SimpleExpression - END - - -
- - -
         ::= 'CASE' Expression? WhenThenSearchCondition+ ( 'ELSE' ( Expression | SimpleExpression ) )? 'END'
-
- Referenced by: -
- - -====================================================================================================================== -WhenThenSearchCondition -====================================================================================================================== - - -.. raw:: html - - - - - - WHEN - - Expression - THEN - - Expression - - SimpleExpression - -
- - -
         ::= 'WHEN' Expression 'THEN' ( Expression | SimpleExpression )
-
- Referenced by: -
- - -====================================================================================================================== -RowConstructor -====================================================================================================================== - - -.. raw:: html - - - - - - ROW - - ParenthesedExpressionList - -
- - -
         ::= 'ROW' ParenthesedExpressionList
-
- Referenced by: -
- - -====================================================================================================================== -VariableExpression -====================================================================================================================== - - -.. raw:: html - - - - - - UserVariable - = - - SimpleExpression - -
- - -
         ::= UserVariable '=' SimpleExpression
-
- Not referenced by any. -
- - -====================================================================================================================== -Execute -====================================================================================================================== - - -.. raw:: html - - - - - - EXEC - - EXECUTE - - CALL - - RelObjectNames - - ExpressionList - -
- -
Execute  ::= ( 'EXEC' | 'EXECUTE' | 'CALL' ) RelObjectNames ExpressionList?
-
- Referenced by: -
- - -====================================================================================================================== -FullTextSearch -====================================================================================================================== - - -.. raw:: html - - - - - - MATCH - - ( - - ColumnList - ) - - AGAINST - - ( - - SimpleExpression - IN NATURAL LANGUAGE MODE - - IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION - - IN BOOLEAN MODE - - WITH QUERY EXPANSION - - ) - - -
- - -
         ::= 'MATCH' '(' ColumnList ')' 'AGAINST' '(' SimpleExpression ( 'IN NATURAL LANGUAGE MODE' | 'IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION' | - 'IN BOOLEAN MODE' | 'WITH QUERY EXPANSION' )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -LambdaExpression -====================================================================================================================== - - -.. raw:: html - - - - - - ParenthesedColumnList - - RelObjectName - -> - - Expression - -
- - -
         ::= ( ParenthesedColumnList | RelObjectName ) '->' Expression
-
- - -====================================================================================================================== -Function -====================================================================================================================== - - -.. raw:: html - - - - - - { - - FN - - InternalFunction - } - - SpecialStringFunctionWithNamedParameters - - InternalFunction - -
- -
Function ::= '{' 'FN' InternalFunction '}'
- -
           | InternalFunction
-
- - -====================================================================================================================== -SpecialStringFunctionWithNamedParameters -====================================================================================================================== - - -.. raw:: html - - - - - - K_STRING_FUNCTION_NAME - ( - - NamedExpressionListExprFirst - - ExpressionList - ) - - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -InternalFunction -====================================================================================================================== - - -.. raw:: html - - - - - - APPROXIMATE - - RelObjectNames - ( - - DISTINCT - - ALL - - UNIQUE - - TABLE - - ExpressionList - - OrderByElements - ON - - OVERFLOW - - TRUNCATE - - ERROR - - S_CHAR_LITERAL - WITH - - WITHOUT - - COUNT - - Select - HAVING - - MIN - - MAX - - Expression - IGNORE - - RESPECT - - NULLS - - PlainLimit - ) - - ( - - ExpressionList - ) - - . - - Function - - Column - IGNORE - - RESPECT - - NULLS - - KeepExpression - -
- - -
         ::= 'APPROXIMATE'? RelObjectNames '(' ( ( 'DISTINCT' | 'ALL' | 'UNIQUE' )? ( 'TABLE'? ExpressionList OrderByElements? ( 'ON' 'OVERFLOW' ( 'TRUNCATE' | 'ERROR' ) ( S_CHAR_LITERAL ( ( 'WITH' | 'WITHOUT' ) 'COUNT' )? )? )? | Select ) )? ( 'HAVING' ( 'MIN' | 'MAX' ) Expression )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? PlainLimit? ')' ( '(' ExpressionList ')' )? ( '.' ( Function | Column ) )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? KeepExpression?
-
- Referenced by: -
- - -====================================================================================================================== -XMLSerializeExpr -====================================================================================================================== - - -.. raw:: html - - - - - - XMLSERIALIZE - - ( - - XMLAGG - - ( - - XMLTEXT - - ( - - SimpleExpression - ) - - OrderByElements - ) - - AS - - ColDataType - ) - - -
- - -
         ::= 'XMLSERIALIZE' '(' 'XMLAGG' '(' 'XMLTEXT' '(' SimpleExpression ')' OrderByElements? ')' 'AS' ColDataType ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonTablePassingClause -====================================================================================================================== - - -.. raw:: html - - - - - - Expression - AS - - RelObjectName - -
- - -
         ::= Expression 'AS' RelObjectName
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableOnEmptyBehavior -====================================================================================================================== - - -.. raw:: html - - - - - - ERROR - - NULL - - TRUE - - FALSE - - DEFAULT - - Expression - - S_IDENTIFIER - - JsonKeyword - ARRAY - - -
- - -
         ::= 'ERROR'
-
           | 'NULL'
-
           | 'TRUE'
-
           | 'FALSE'
-
           | 'DEFAULT' Expression
-
           | S_IDENTIFIER ( JsonKeyword | 'ARRAY' )?
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableWrapperClause -====================================================================================================================== - - -.. raw:: html - - - - - - WITHOUT - - WITH - - S_IDENTIFIER - ARRAY - - JsonKeyword - -
- - -
         ::= ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableQuotesClause -====================================================================================================================== - - -.. raw:: html - - - - - - KEEP - - JsonKeyword - - JsonKeyword - ON - - JsonKeyword - STRING - - -
- - -
         ::= ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )?
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableColumnDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - JsonKeyword - PATH - - Expression - AS - - RelObjectName - - JsonTableColumnsClause - - RelObjectName - FOR - - ORDINALITY - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - - JsonKeyword - - JsonKeyword - EXISTS - - JsonTableWrapperClause - PATH - - Expression - - JsonTableWrapperClause - - JsonTableQuotesClause - - JsonTableOnEmptyBehavior - ON - - EMPTY - - JsonQueryOnResponseBehavior - ON - - ERROR - - JsonTableOnEmptyBehavior - ON - - EMPTY - - -
- - -
         ::= JsonKeyword 'PATH'? Expression ( 'AS' RelObjectName )? JsonTableColumnsClause
-
           | RelObjectName ( 'FOR' 'ORDINALITY' | ColDataType? ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? ( JsonKeyword JsonKeyword )? 'EXISTS'? JsonTableWrapperClause? ( 'PATH' Expression )? JsonTableWrapperClause? JsonTableQuotesClause? ( JsonTableOnEmptyBehavior 'ON' 'EMPTY' )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? ( JsonTableOnEmptyBehavior 'ON' 'EMPTY' )? )
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableColumnsClause -====================================================================================================================== - - -.. raw:: html - - - - - - COLUMNS - - ( - - JsonTableColumnDefinition - , - - ) - - -
- - -
         ::= 'COLUMNS' '(' ( JsonTableColumnDefinition ( ',' JsonTableColumnDefinition )* )? ')'
-
- - -====================================================================================================================== -JsonTablePlanTerm -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - JsonTablePlanExpression - ) - - RelObjectName - - Expression - -
- - -
         ::= '(' JsonTablePlanExpression ')'
-
           | RelObjectName
-
           | Expression
-
- Referenced by: -
- - -====================================================================================================================== -JsonTablePlanExpression -====================================================================================================================== - - -.. raw:: html - - - - - - JsonTablePlanTerm - , - - INNER - - OUTER - - CROSS - - UNION - - -
- - -
         ::= JsonTablePlanTerm ( ( ',' | 'INNER' | 'OUTER' | 'CROSS' | 'UNION' ) JsonTablePlanTerm )*
-
- - -====================================================================================================================== -JsonTablePlanClause -====================================================================================================================== - - -.. raw:: html - - - - - - PLAN - - DEFAULT - - ( - - JsonTablePlanExpression - ) - - -
- - -
         ::= 'PLAN' 'DEFAULT'? '(' JsonTablePlanExpression ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableOnErrorClause -====================================================================================================================== - - -.. raw:: html - - - - - - ERROR - - EMPTY - - TRUE - - FALSE - - NULL - - ON - - ERROR - - -
- - -
         ::= ( 'ERROR' | 'EMPTY' | 'TRUE' | 'FALSE' | 'NULL' ) 'ON' 'ERROR'
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableOnEmptyClause -====================================================================================================================== - - -.. raw:: html - - - - - - ERROR - - EMPTY - - TRUE - - FALSE - - NULL - - ON - - EMPTY - - -
- - -
         ::= ( 'ERROR' | 'EMPTY' | 'TRUE' | 'FALSE' | 'NULL' ) 'ON' 'EMPTY'
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableParsingTypeClause -====================================================================================================================== - - -.. raw:: html - - - - - - TYPE - - ( - - STRICT - - JsonKeyword - ) - - -
- - -
         ::= 'TYPE' '(' ( 'STRICT' | JsonKeyword ) ')'
-
- Referenced by: -
- - -====================================================================================================================== -JsonTableBody -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Expression - FORMAT - - JSON - - , - - Expression - AS - - RelObjectName - - JsonKeyword - - JsonTablePassingClause - , - - JsonTableOnErrorClause - - JsonTableParsingTypeClause - - JsonTableOnEmptyClause - - JsonTableColumnsClause - - JsonTablePlanClause - - JsonTableOnErrorClause - ) - - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -TableFunction -====================================================================================================================== - - -.. raw:: html - - - - - - LATERAL - - JsonKeyword - - JsonTableBody - - Function - WITH - - OFFSET - - ORDINALITY - - -
- - -
         ::= 'LATERAL'? ( JsonKeyword JsonTableBody | Function ) ( 'WITH' ( 'OFFSET' | 'ORDINALITY' ) )?
-
- Referenced by: -
- - -====================================================================================================================== -ColumnNamesWithParamsList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - RelObjectName - - CreateParameter - , - - ) - - -
- - -
         ::= '(' RelObjectName CreateParameter? ( ',' RelObjectName CreateParameter? )* ')'
-
- - -====================================================================================================================== -IndexColumnWithParams -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ( - - Expression - ) - - CreateParameter - -
- - -
         ::= ( RelObjectName | '(' Expression ')' ) CreateParameter?
-
- Referenced by: -
- - -====================================================================================================================== -IndexColumnsWithParamsList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - IndexColumnWithParams - , - - ) - - -
- - -
         ::= '(' IndexColumnWithParams ( ',' IndexColumnWithParams )* ')'
-
- - -====================================================================================================================== -Index -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNames - -
- - -
- Referenced by: -
- - -====================================================================================================================== -CreateIndex -====================================================================================================================== - - -.. raw:: html - - - - - - CreateParameter - INDEX - - IF - - NOT - - EXISTS - - Index - ON - - Table - - UsingIndexType - - UsingIndexType - ON - - Table - - IndexColumnsWithParamsList - - CreateParameter - -
- - -
         ::= CreateParameter? 'INDEX' ( 'IF' 'NOT' 'EXISTS' )? Index ( 'ON' Table UsingIndexType? | UsingIndexType? 'ON' Table ) IndexColumnsWithParamsList CreateParameter*
-
- Referenced by: -
- - -====================================================================================================================== -ColumnDefinition -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - - ColDataType - - CreateParameter - -
- - - -
- - -====================================================================================================================== -CreateSchema -====================================================================================================================== - - -.. raw:: html - - - - - - SCHEMA - - IF - - NOT - - EXISTS - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - . - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - AUTHORIZATION - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - PathSpecification - CREATE - - CreateTable - - CreateView - -
- - -
         ::= 'SCHEMA' ( 'IF' 'NOT' 'EXISTS' )? ( ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ( '.' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) )? )? ( 'AUTHORIZATION' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) )? PathSpecification? ( 'CREATE' CreateTable | CreateView )*
-
- Referenced by: -
- - -====================================================================================================================== -PathSpecification -====================================================================================================================== - - -.. raw:: html - - - - - - PATH - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - , - - -
- - -
         ::= 'PATH' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ( ',' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) )*
-
- Referenced by: -
- - -====================================================================================================================== -CreateTableConstraint -====================================================================================================================== - - -.. raw:: html - - - - - - INDEX - - UNIQUE - - FULLTEXT - - SPATIAL - - KEY - - RelObjectName - - IndexColumnsWithParamsList - - CreateParameter - CONSTRAINT - - RelObjectName - PRIMARY - - KEY - - UNIQUE - - KEY - - ColumnNamesWithParamsList - - CreateParameter - - ForeignKeySpec - - CheckConstraintSpec - EXCLUDE - - WHERE - - ( - - Expression - ) - - -
- - -
         ::= ( 'INDEX' | 'UNIQUE'? ( 'FULLTEXT' | 'SPATIAL' )? 'KEY' ) RelObjectName IndexColumnsWithParamsList CreateParameter*
-
           | ( 'CONSTRAINT' RelObjectName )? ( ( 'PRIMARY' 'KEY' | 'UNIQUE' 'KEY'? ) ColumnNamesWithParamsList CreateParameter* | ForeignKeySpec | CheckConstraintSpec )
-
           | 'EXCLUDE' 'WHERE' ( '(' Expression ')' )*
-
- Referenced by: -
- - -====================================================================================================================== -CreateTable -====================================================================================================================== - - -.. raw:: html - - - - - - UNLOGGED - - GLOBAL - - CreateParameter - TABLE - - IF - - NOT - - EXISTS - - Table - ( - - RelObjectName - , - - ColumnDefinition - , - - CreateTableConstraint - - ColumnDefinition - ) - - CreateParameter - - RowMovement - AS - - Select - LIKE - - ( - - Table - ) - - Table - , - - SpannerInterleaveIn - -
- - -
         ::= 'UNLOGGED'? 'GLOBAL'? CreateParameter* 'TABLE' ( 'IF' 'NOT' 'EXISTS' )? Table ( '(' ( RelObjectName ( ',' RelObjectName )* | ColumnDefinition ( ',' ( CreateTableConstraint | ColumnDefinition ) )* ) ')' )? CreateParameter* RowMovement? ( 'AS' Select )? ( 'LIKE' ( '(' Table ')' | Table ) )? ( ',' SpannerInterleaveIn )?
-
- Referenced by: -
- - -====================================================================================================================== -SpannerInterleaveIn -====================================================================================================================== - - -.. raw:: html - - - - - - INTERLEAVE - - IN - - PARENT - - Table - ON - - DELETE - - NO - - ACTION - - CASCADE - - -
- - -
         ::= 'INTERLEAVE' 'IN' 'PARENT' Table ( 'ON' 'DELETE' ( 'NO' 'ACTION' | 'CASCADE' ) )?
-
- Referenced by: -
- - -====================================================================================================================== -DataType -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATETIMELITERAL - - DT_ZONE - - DATA_TYPE - SIGNED - - UNSIGNED - - CHARACTER - - BIT - - BYTES - - BINARY - - BOOLEAN - - CHAR - - JSON - - STRING - - DATA_TYPE - SIGNED - - UNSIGNED - - CHARACTER - - BIT - - BYTES - - BINARY - - BOOLEAN - - CHAR - - JSON - - STRING - - ( - - S_LONG - MAX - - BYTE - - CHAR - - , - - S_LONG - ) - - K_TEXT_LITERAL - ARRAY - - < - - ColDataType - > - - -
- - -
           | 'ARRAY' '<' ColDataType '>'
-
           | ( K_DATETIMELITERAL | DT_ZONE | DATA_TYPE | 'SIGNED' | 'UNSIGNED' | 'CHARACTER' | 'BIT' | 'BYTES' | 'BINARY' | 'BOOLEAN' | - 'CHAR' | 'JSON' | 'STRING' ) ( DATA_TYPE | 'SIGNED' | 'UNSIGNED' | 'CHARACTER' | 'BIT' | 'BYTES' | 'BINARY' | 'BOOLEAN' | - 'CHAR' | 'JSON' | 'STRING' )* ( '(' ( S_LONG | 'MAX' ) ( 'BYTE' | 'CHAR' )? ( ',' S_LONG )? ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -ColDataType -====================================================================================================================== - - -.. raw:: html - - - - - - STRUCT - - ( - - RelObjectNameExt - - ColDataType - , - - ) - - DataType - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - K_DATETIMELITERAL - - K_DATE_LITERAL - XML - - INTERVAL - - DT_ZONE - CHAR - - SET - - BINARY - - JSON - - STRING - - PUBLIC - - DATA - - NAME - - . - - ColDataType - ( - - S_LONG - MAX - - BYTE - - CHAR - - S_CHAR_LITERAL - - S_IDENTIFIER - CHAR - - , - - ) - - [ - - S_LONG - ] - - CHARACTER - - SET - - S_IDENTIFIER - BINARY - - -
- - -
         ::= ( 'STRUCT' '(' RelObjectNameExt ColDataType ( ',' RelObjectNameExt ColDataType )* ')' | DataType | ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | K_DATETIMELITERAL | K_DATE_LITERAL | 'XML' | 'INTERVAL' | DT_ZONE | 'CHAR' | 'SET' | 'BINARY' | 'JSON' | 'STRING' | 'PUBLIC' | 'DATA' | 'NAME' ) ( - '.' ColDataType )? ) ( '(' ( ( ( S_LONG | 'MAX' ) ( 'BYTE' | 'CHAR' )? | S_CHAR_LITERAL | S_IDENTIFIER | 'CHAR' ) ','? )* ')' )? ( '[' S_LONG? ']' )* ( 'CHARACTER' 'SET' ( S_IDENTIFIER | 'BINARY' ) )?
-
- - -====================================================================================================================== -Analyze -====================================================================================================================== - - -.. raw:: html - - - - - - ANALYZE - - Table - -
- -
Analyze  ::= 'ANALYZE' Table
-
- Referenced by: -
- - -====================================================================================================================== -ColumnWithCommentList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - Column - , - - ) - - -
- - -
         ::= '(' Column ( ',' Column )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -CreateView -====================================================================================================================== - - -.. raw:: html - - - - - - NO - - FORCE - - SECURE - - TEMP - - TEMPORARY - - VOLATILE - - MATERIALIZED - - VIEW - - Table - AUTO - - REFRESH - - YES - - NO - - IF - - NOT - - EXISTS - - ColumnWithCommentList - - CreateViewTailComment - AS - - Select - WITH - - READ - - ONLY - - -
- - -
         ::= ( 'NO'? 'FORCE' )? 'SECURE'? ( 'TEMP' | 'TEMPORARY' | 'VOLATILE' )? 'MATERIALIZED'? - 'VIEW' Table ( 'AUTO' 'REFRESH' ( 'YES' | 'NO' ) )? ( 'IF' 'NOT' 'EXISTS' )? ColumnWithCommentList? CreateViewTailComment? 'AS' Select ( 'WITH' 'READ' 'ONLY' )?
-
- Referenced by: -
- - -====================================================================================================================== -CreateViewTailComment -====================================================================================================================== - - -.. raw:: html - - - - - - COMMENT - - = - - S_CHAR_LITERAL - -
- - -
         ::= 'COMMENT' '='? S_CHAR_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -Action -====================================================================================================================== - - -.. raw:: html - - - - - - CASCADE - - RESTRICT - - NO - - ACTION - - SET - - NULL - - DEFAULT - - -
- -
Action   ::= 'CASCADE'
-
           | 'RESTRICT'
-
           | 'NO' 'ACTION'
-
           | 'SET' ( 'NULL' | 'DEFAULT' )
-
- Referenced by: -
- - -====================================================================================================================== -ReferentialActionsOnIndex -====================================================================================================================== - - -.. raw:: html - - - - - - ON - - DELETE - - UPDATE - - Action - ON - - DELETE - - UPDATE - - Action - -
- - -
         ::= ( 'ON' ( 'DELETE' | 'UPDATE' ) Action )? ( 'ON' ( 'DELETE' | 'UPDATE' ) Action )?
-
- - -====================================================================================================================== -CheckConstraintSpec -====================================================================================================================== - - -.. raw:: html - - - - - - CHECK - - ( - - Expression - ) - - -
- - -
         ::= 'CHECK' ( '(' Expression ')' )*
-
- - -====================================================================================================================== -ForeignKeySpec -====================================================================================================================== - - -.. raw:: html - - - - - - FOREIGN - - KEY - - ColumnNamesWithParamsList - REFERENCES - - Table - - ColumnsNamesList - - ReferentialActionsOnIndex - -
- - -
         ::= 'FOREIGN' 'KEY' ColumnNamesWithParamsList 'REFERENCES' Table ColumnsNamesList? ReferentialActionsOnIndex
-
- - -====================================================================================================================== -AlterExpressionUsingIndex -====================================================================================================================== - - -.. raw:: html - - - - - - USING - - INDEX - - RelObjectName - -
- - -
         ::= 'USING' 'INDEX'? RelObjectName
-
- - -====================================================================================================================== -AlterExpressionConstraintTail -====================================================================================================================== - - -.. raw:: html - - - - - - AlterExpressionConstraintState - - AlterExpressionUsingIndex - - IndexWithComment - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -AlterView -====================================================================================================================== - - -.. raw:: html - - - - - - VIEW - - Table - - ColumnsNamesList - AS - - Select - -
- - -
         ::= 'VIEW' Table ColumnsNamesList? 'AS' Select
-
- Referenced by: -
- - -====================================================================================================================== -CreateParameter -====================================================================================================================== - - -.. raw:: html - - - - - - K_NEXTVAL - ( - - S_CHAR_LITERAL - :: - - ColDataType - ) - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - NAME - - . - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - NAME - - USING - - INDEX - - TABLESPACE - - RelObjectName - - S_CHAR_LITERAL - NULL - - NOT - - AUTO_INCREMENT - - PRIMARY - - FOREIGN - - REFERENCES - - KEY - - STORED - - ON - - COMMIT - - DROP - - ROWS - - UNIQUE - - CASCADE - - DELETE - - UPDATE - - CONSTRAINT - - WITH - - EXCLUDE - - WHERE - - TEMP - - TEMPORARY - - PARTITION - - BY - - IN - - TYPE - - COMMENT - - USING - - COLLATE - - ASC - - DESC - - TRUE - - FALSE - - PARALLEL - - BINARY - - START - - ORDER - - K_TIME_KEY_EXPR - RAW - - HASH - - FIRST - - LAST - - SIGNED - - UNSIGNED - - ENGINE - - IDENTITY - - MATERIALIZED - - SAMPLE - - ALWAYS - - = - - DEFAULT - - AS - - CHECK - - ( - - Expression - ) - - + - - - - - S_LONG - - S_DOUBLE - - AList - CHARACTER - - SET - - ARRAY - - ArrayConstructor - :: - - ColDataType - -
- - -
         ::= K_NEXTVAL '(' S_CHAR_LITERAL '::' ColDataType ')'
-
           | ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | 'NAME' ) ( '.' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | 'NAME' ) )?
-
           | ( 'USING' 'INDEX' )? 'TABLESPACE' RelObjectName
-
           | S_CHAR_LITERAL
-
           | 'NULL'
-
           | 'NOT'
-
           | 'AUTO_INCREMENT'
-
           | 'PRIMARY'
-
           | 'FOREIGN'
-
           | 'REFERENCES'
-
           | 'KEY'
-
           | 'STORED'
-
           | 'ON'
-
           | 'COMMIT'
-
           | 'DROP'
-
           | 'ROWS'
-
           | 'UNIQUE'
-
           | 'CASCADE'
-
           | 'DELETE'
-
           | 'UPDATE'
-
           | 'CONSTRAINT'
-
           | 'WITH'
-
           | 'EXCLUDE'
-
           | 'WHERE'
-
           | 'TEMP'
-
           | 'TEMPORARY'
-
           | 'PARTITION'
-
           | 'BY'
-
           | 'IN'
-
           | 'TYPE'
-
           | 'COMMENT'
-
           | 'USING'
-
           | 'COLLATE'
-
           | 'ASC'
-
           | 'DESC'
-
           | 'TRUE'
-
           | 'FALSE'
-
           | 'PARALLEL'
-
           | 'BINARY'
-
           | 'START'
-
           | 'ORDER'
-
           | K_TIME_KEY_EXPR
-
           | 'RAW'
-
           | 'HASH'
-
           | 'FIRST'
-
           | 'LAST'
-
           | 'SIGNED'
-
           | 'UNSIGNED'
-
           | 'ENGINE'
-
           | 'IDENTITY'
-
           | 'MATERIALIZED'
-
           | 'SAMPLE'
-
           | 'ALWAYS'
-
           | '='
-
           | ( 'DEFAULT' | 'AS' | 'CHECK' ) ( '(' Expression ')' )?
-
           | ( '+' | '-' )? S_LONG
-
           | S_DOUBLE
-
           | AList
-
           | 'CHARACTER' 'SET'
-
           | 'ARRAY' ArrayConstructor
-
           | '::' ColDataType
-
- - -====================================================================================================================== -RowMovement -====================================================================================================================== - - -.. raw:: html - - - - - - ENABLE - - DISABLE - - ROW - - MOVEMENT - - -
- - -
         ::= ( 'ENABLE' | 'DISABLE' ) 'ROW' 'MOVEMENT'
-
- Referenced by: -
- - -====================================================================================================================== -AList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - S_LONG - - S_DOUBLE - - S_CHAR_LITERAL - TRUE - - FALSE - - RelObjectName - , - - = - - ) - - -
- -
AList    ::= '(' ( ( S_LONG | S_DOUBLE | S_CHAR_LITERAL | 'TRUE' | 'FALSE' | RelObjectName ) ( ',' | '=' )? )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -ColumnsNamesListItem -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - ( - - S_LONG - ) - - ASC - - DESC - - -
- - -
         ::= RelObjectName ( '(' S_LONG ')' )? ( 'ASC' | 'DESC' )?
-
- Referenced by: -
- - -====================================================================================================================== -ColumnsNamesList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - ColumnsNamesListItem - , - - ) - - -
- - -
         ::= '(' ColumnsNamesListItem ( ',' ColumnsNamesListItem )* ')'
-
- - -====================================================================================================================== -FuncArgsListItem -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - - RelObjectName - ( - - S_LONG - ) - - -
- - -
         ::= RelObjectName RelObjectName? ( '(' S_LONG ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -FuncArgsList -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - FuncArgsListItem - , - - ) - - -
- - -
         ::= '(' ( FuncArgsListItem ( ',' FuncArgsListItem )* )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -Drop -====================================================================================================================== - - -.. raw:: html - - - - - - DROP - - MATERIALIZED - - S_IDENTIFIER - TEMPORARY - - TABLE - - INDEX - - VIEW - - SCHEMA - - SEQUENCE - - FUNCTION - - IF - - EXISTS - - Table - - FuncArgsList - - S_IDENTIFIER - CASCADE - - RESTRICT - - ON - - Table - -
- -
Drop     ::= 'DROP' 'MATERIALIZED'? ( S_IDENTIFIER | 'TEMPORARY'? 'TABLE' | 'INDEX' | 'VIEW' | 'SCHEMA' | 'SEQUENCE' | 'FUNCTION' ) - ( 'IF' 'EXISTS' )? Table FuncArgsList? ( S_IDENTIFIER | 'CASCADE' | 'RESTRICT' | 'ON' Table )*
-
- Referenced by: -
- - -====================================================================================================================== -Truncate -====================================================================================================================== - - -.. raw:: html - - - - - - TRUNCATE - - TABLE - - ONLY - - Table - , - - CASCADE - - -
- -
Truncate ::= 'TRUNCATE' 'TABLE'? 'ONLY'? Table ( ',' Table )* 'CASCADE'?
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnChanges -====================================================================================================================== - - -.. raw:: html - - - - - - AlterExpressionColumnDropDefault - - AlterExpressionColumnSetDefault - - AlterExpressionColumnSetVisibility - ( - - AlterExpressionColumnDataType - , - - ) - - -
- - -
         ::= AlterExpressionColumnDropDefault
-
           | AlterExpressionColumnSetDefault
-
           | AlterExpressionColumnSetVisibility
-
           | '(' AlterExpressionColumnDataType ( ',' AlterExpressionColumnDataType )* ')'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnDataType -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - TYPE - - ColDataType - - CreateParameter - -
- - -
         ::= RelObjectName 'TYPE'? ColDataType? CreateParameter*
-
- - -====================================================================================================================== -AlterExpressionColumnDropNotNull -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - DROP - - NOT - - NULL - - -
- - -
         ::= RelObjectName 'DROP' 'NOT'? 'NULL'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnDropDefault -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - DROP - - DEFAULT - - -
- - -
         ::= RelObjectName 'DROP' 'DEFAULT'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnSetDefault -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - SET - - DEFAULT - - Expression - -
- - -
         ::= RelObjectName 'SET' 'DEFAULT' Expression
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionColumnSetVisibility -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - SET - - VISIBLE - - INVISIBLE - - -
- - -
         ::= RelObjectName 'SET' ( 'VISIBLE' | 'INVISIBLE' )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionConstraintState -====================================================================================================================== - - -.. raw:: html - - - - - - NOT - - DEFERRABLE - - VALIDATE - - NOVALIDATE - - ENABLE - - DISABLE - - -
- - -
         ::= ( 'NOT'? 'DEFERRABLE' | 'VALIDATE' | 'NOVALIDATE' | 'ENABLE' | 'DISABLE' - )*
-
- - -====================================================================================================================== -IndexWithComment -====================================================================================================================== - - -.. raw:: html - - - - - - COMMENT - - S_CHAR_LITERAL - -
- - -
         ::= 'COMMENT' S_CHAR_LITERAL
-
- - -====================================================================================================================== -IndexOptionList -====================================================================================================================== - - -.. raw:: html - - - - - - IndexOption - -
- - -
         ::= IndexOption*
-
- Referenced by: -
- - -====================================================================================================================== -UsingIndexType -====================================================================================================================== - - -.. raw:: html - - - - - - USING - - RelObjectName - -
- - -
         ::= 'USING' RelObjectName
-
- - -====================================================================================================================== -IndexOption -====================================================================================================================== - - -.. raw:: html - - - - - - KEY_BLOCK_SIZE - - = - - S_LONG - WITH - - PARSER - - S_IDENTIFIER - COMMENT - - S_CHAR_LITERAL - VISIBLE - - INVISIBLE - - UsingIndexType - -
- - -
         ::= 'KEY_BLOCK_SIZE' '='? S_LONG
-
           | 'WITH' 'PARSER' S_IDENTIFIER
-
           | 'COMMENT' S_CHAR_LITERAL
-
           | 'VISIBLE'
-
           | 'INVISIBLE'
-
           | UsingIndexType
-
- Referenced by: -
- - -====================================================================================================================== -PartitionDefinitions -====================================================================================================================== - - -.. raw:: html - - - - - - ( - - PARTITION - - RelObjectName - VALUES - - LESS - - THAN - - ( - - Expression - ) - - MAXVALUE - - ENGINE - - = - - S_IDENTIFIER - , - - ) - - -
- - -
         ::= '(' ( 'PARTITION' RelObjectName 'VALUES' 'LESS' 'THAN' ( '(' Expression ')' | 'MAXVALUE' ) ( 'ENGINE' '=' S_IDENTIFIER )? ','? )* ')'
-
- - -====================================================================================================================== -PartitionNamesList -====================================================================================================================== - - -.. raw:: html - - - - - - ALL - - S_IDENTIFIER - , - - -
- - -
         ::= 'ALL'
-
           | S_IDENTIFIER ( ',' S_IDENTIFIER )*
-
- - -====================================================================================================================== -AlterExpressionDiscardOrImport -====================================================================================================================== - - -.. raw:: html - - - - - - DISCARD - - IMPORT - - PARTITION - - PartitionNamesList - TABLESPACE - - -
- - -
         ::= ( 'DISCARD' | 'IMPORT' ) ( 'PARTITION' PartitionNamesList )? 'TABLESPACE'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionAddConstraint -====================================================================================================================== - - -.. raw:: html - - - - - - CONSTRAINT - - UNIQUE - - KEY - - INDEX - - RelObjectName - - ColumnsNamesList - - RelObjectName - FOREIGN - - KEY - - ColumnsNamesList - REFERENCES - - Table - - ColumnsNamesList - - ReferentialActionsOnIndex - KEY - - ColumnsNamesList - - AlterExpressionConstraintState - PRIMARY - - KEY - - UNIQUE - - KEY - - INDEX - - ColumnsNamesList - - AlterExpressionConstraintTail - NOT - - ENFORCED - - CheckConstraintSpec - -
- - -
         ::= 'CONSTRAINT' ( 'UNIQUE' ( 'KEY' | 'INDEX' )? RelObjectName ColumnsNamesList | RelObjectName ( ( 'FOREIGN' 'KEY' ColumnsNamesList 'REFERENCES' Table ColumnsNamesList? ReferentialActionsOnIndex | 'KEY' ColumnsNamesList ) AlterExpressionConstraintState | ( 'PRIMARY' 'KEY' | 'UNIQUE' ( 'KEY' | 'INDEX' )? ) ColumnsNamesList AlterExpressionConstraintTail | 'NOT'? 'ENFORCED' | CheckConstraintSpec ) )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionDrop -====================================================================================================================== - - -.. raw:: html - - - - - - DROP - - PARTITION - - PartitionNamesList - - ColumnsNamesList - COLUMN - - IF - - EXISTS - - KeywordOrIdentifier - INVALIDATE - - CASCADE - - CONSTRAINTS - - INDEX - - KEY - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - UNIQUE - - FOREIGN - - KEY - - ColumnsNamesList - PRIMARY - - KEY - - CONSTRAINT - - IF - - EXISTS - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - CASCADE - - RESTRICT - - -
- - -
         ::= 'DROP' ( 'PARTITION' PartitionNamesList | ( ColumnsNamesList | 'COLUMN'? ( 'IF' 'EXISTS' )? KeywordOrIdentifier ) 'INVALIDATE'? ( 'CASCADE' 'CONSTRAINTS'? )? | ( 'INDEX' | 'KEY' ) ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) | ( ( 'UNIQUE' | 'FOREIGN' 'KEY' ) ColumnsNamesList | 'PRIMARY' 'KEY' | 'CONSTRAINT' ( 'IF' 'EXISTS' )? ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ) ( 'CASCADE' | 'RESTRICT' )? )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionPartitionOp -====================================================================================================================== - - -.. raw:: html - - - - - - TRUNCATE - - ANALYZE - - CHECK - - OPTIMIZE - - REBUILD - - REPAIR - - PARTITION - - PartitionNamesList - COALESCE - - PARTITION - - S_LONG - REORGANIZE - - PARTITION - - PartitionNamesList - INTO - - PARTITION - - BY - - RANGE - - ( - - Expression - ) - - COLUMNS - - ColumnsNamesList - - PartitionDefinitions - EXCHANGE - - PARTITION - - PartitionNamesList - WITH - - TABLE - - S_IDENTIFIER - WITH - - WITHOUT - - VALIDATION - - REMOVE - - PARTITIONING - - -
- - -
         ::= ( 'TRUNCATE' | 'ANALYZE' | 'CHECK' | 'OPTIMIZE' | 'REBUILD' | 'REPAIR' - ) 'PARTITION' PartitionNamesList
-
           | 'COALESCE' 'PARTITION' S_LONG
-
           | ( 'REORGANIZE' 'PARTITION' PartitionNamesList 'INTO' | 'PARTITION' 'BY' 'RANGE' ( '(' Expression ')' | 'COLUMNS' ColumnsNamesList ) ) PartitionDefinitions
-
           | 'EXCHANGE' 'PARTITION' PartitionNamesList 'WITH' 'TABLE' S_IDENTIFIER ( ( 'WITH' | 'WITHOUT' ) 'VALIDATION' )?
-
           | 'REMOVE' 'PARTITIONING'
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionAddAlterModify -====================================================================================================================== - - -.. raw:: html - - - - - - ADD - - ALTER - - MODIFY - - PRIMARY - - KEY - - ColumnsNamesList - - AlterExpressionConstraintState - - AlterExpressionUsingIndex - KEY - - INDEX - - RelObjectName - - UsingIndexType - - IndexColumnsWithParamsList - - IndexOptionList - - AlterExpressionConstraintState - SPATIAL - - FULLTEXT - - INDEX - - KEY - - RelObjectName - - ColumnsNamesList - - IndexOptionList - - RelObjectName - COMMENT - - S_CHAR_LITERAL - PARTITION - - PartitionDefinitions - COLUMN - - COLUMNS - - IF - - NOT - - EXISTS - - AlterExpressionColumnChanges - - AlterExpressionColumnDataType - - AlterExpressionColumnDropNotNull - - AlterExpressionColumnChanges - UNIQUE - - KEY - - INDEX - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - - ColumnsNamesList - - AlterExpressionUsingIndex - - IndexWithComment - - ForeignKeySpec - CHECK - - RelObjectName - NOT - - ENFORCED - - AlterExpressionAddConstraint - -
- - -
         ::= ( 'ADD' | 'ALTER' | 'MODIFY' ) ( 'PRIMARY' 'KEY' ColumnsNamesList AlterExpressionConstraintState AlterExpressionUsingIndex? | ( 'KEY' | 'INDEX' ) RelObjectName? UsingIndexType? IndexColumnsWithParamsList? IndexOptionList AlterExpressionConstraintState | ( 'SPATIAL' | 'FULLTEXT' ) ( 'INDEX' | 'KEY' )? RelObjectName? ColumnsNamesList IndexOptionList | RelObjectName 'COMMENT' S_CHAR_LITERAL | 'PARTITION' PartitionDefinitions | ( 'COLUMN' | 'COLUMNS' )? ( 'IF' 'NOT' 'EXISTS' )? ( AlterExpressionColumnChanges | AlterExpressionColumnDataType | AlterExpressionColumnDropNotNull ) | AlterExpressionColumnChanges | 'UNIQUE' ( 'KEY' | 'INDEX' )? ( S_IDENTIFIER | S_QUOTED_IDENTIFIER )? ColumnsNamesList AlterExpressionUsingIndex? IndexWithComment? | ForeignKeySpec | 'CHECK' RelObjectName 'NOT'? 'ENFORCED' | AlterExpressionAddConstraint )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpressionRenameOp -====================================================================================================================== - - -.. raw:: html - - - - - - RENAME - - INDEX - - KEY - - CONSTRAINT - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - TO - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - COLUMN - - KeywordOrIdentifier - TO - - KeywordOrIdentifier - -
- - -
         ::= 'RENAME' ( ( ( 'INDEX' | 'KEY' | 'CONSTRAINT' ) ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) )? 'TO' ( S_IDENTIFIER | S_QUOTED_IDENTIFIER ) | 'COLUMN'? KeywordOrIdentifier 'TO' KeywordOrIdentifier )
-
- Referenced by: -
- - -====================================================================================================================== -AlterExpression -====================================================================================================================== - - -.. raw:: html - - - - - - AlterExpressionAddAlterModify - CHANGE - - COLUMN - - KeywordOrIdentifier - - AlterExpressionColumnDataType - - AlterExpressionDrop - FORCE - - ROW - - LEVEL - - SECURITY - - NO - - FORCE - - ROW - - LEVEL - - SECURITY - - ALGORITHM - - LOCK - - ENGINE - - = - - RelObjectName - KEY_BLOCK_SIZE - - AUTO_INCREMENT - - = - - S_LONG - - AlterExpressionRenameOp - CONVERT - - TO - - CHARACTER - - SET - - S_IDENTIFIER - COLLATE - - DEFAULT - - CHARACTER - - SET - - = - - S_IDENTIFIER - COLLATE - - COLLATE - - = - - S_IDENTIFIER - COMMENT - - ENCRYPTION - - = - - S_CHAR_LITERAL - - AlterExpressionDiscardOrImport - DISABLE - - ENABLE - - ROW - - LEVEL - - SECURITY - - KEYS - - AlterExpressionPartitionOp - - captureRest - -
- - -
         ::= AlterExpressionAddAlterModify
-
           | 'CHANGE' 'COLUMN'? KeywordOrIdentifier AlterExpressionColumnDataType
-
           | AlterExpressionDrop
-
           | 'FORCE' ( 'ROW' 'LEVEL' 'SECURITY' )?
-
           | 'NO' 'FORCE' 'ROW' 'LEVEL' 'SECURITY'
-
           | ( 'ALGORITHM' | 'LOCK' | 'ENGINE' ) '='? RelObjectName
-
           | ( 'KEY_BLOCK_SIZE' | 'AUTO_INCREMENT' ) '='? S_LONG
-
           | AlterExpressionRenameOp
-
           | ( 'CONVERT' 'TO' 'CHARACTER' 'SET' ( S_IDENTIFIER 'COLLATE' )? | 'DEFAULT'? ( 'CHARACTER' 'SET' ( '='? S_IDENTIFIER 'COLLATE' )? | 'COLLATE' ) '='? ) S_IDENTIFIER
-
           | ( 'COMMENT' | 'ENCRYPTION' ) '='? S_CHAR_LITERAL
-
           | AlterExpressionDiscardOrImport
-
           | ( 'DISABLE' | 'ENABLE' ) ( 'ROW' 'LEVEL' 'SECURITY' | 'KEYS' )
-
           | AlterExpressionPartitionOp
-
           | captureRest
-
- Referenced by: -
- - -====================================================================================================================== -Alter -====================================================================================================================== - - -.. raw:: html - - - - - - ALTER - - AlterTable - - AlterSession - - AlterView - - AlterSystemStatement - - AlterSequence - - captureRest - REPLACE - - AlterView - - captureRest - -
- - -
           | 'REPLACE' ( AlterView | captureRest )
-
- Referenced by: -
- - -====================================================================================================================== -AlterTable -====================================================================================================================== - - -.. raw:: html - - - - - - TABLE - - ONLY - - IF - - EXISTS - - Table - - AlterExpression - , - - -
- - -
         ::= 'TABLE' 'ONLY'? ( 'IF' 'EXISTS' )? Table AlterExpression ( ',' AlterExpression )*
-
- Referenced by: -
- - -====================================================================================================================== -AlterSession -====================================================================================================================== - - -.. raw:: html - - - - - - SESSION - - ADVISE - - COMMIT - - ROLLBACK - - NOTHING - - CLOSE - - DATABASE - - LINK - - ENABLE - - DISABLE - - COMMIT - - IN - - PROCEDURE - - GUARD - - PARALLEL - - DML - - DDL - - QUERY - - RESUMABLE - - FORCE - - PARALLEL - - DML - - DDL - - QUERY - - SET - - S_CHAR_LITERAL - - S_IDENTIFIER - = - - S_LONG - PARALLEL - - -
- - -
         ::= 'SESSION' ( 'ADVISE' ( 'COMMIT' | 'ROLLBACK' | 'NOTHING' ) | 'CLOSE' - 'DATABASE' 'LINK' | ( 'ENABLE' | 'DISABLE' ) ( 'COMMIT' 'IN' 'PROCEDURE' | 'GUARD' - | 'PARALLEL' ( 'DML' | 'DDL' | 'QUERY' ) | 'RESUMABLE' ) | 'FORCE' 'PARALLEL' ( 'DML' - | 'DDL' | 'QUERY' ) | 'SET' ) ( S_CHAR_LITERAL | S_IDENTIFIER | '=' | S_LONG | 'PARALLEL' )*
-
- Referenced by: -
- - -====================================================================================================================== -AlterSystemStatement -====================================================================================================================== - - -.. raw:: html - - - - - - SYSTEM - - ARCHIVE - - LOG - - CHECKPOINT - - DUMP - - ACTIVE - - SESSION - - HISTORY - - ENABLE - - DISABLE - - DISTRIBUTED - - RECOVERY - - RESTRICTED - - SESSION - - FLUSH - - DISCONNECT - - KILL - - SESSION - - SWITCH - - SUSPEND - - RESUME - - QUIESCE - - RESTRICTED - - UNQIESCE - - SHUTDOWN - - REGISTER - - SET - - RESET - - captureRest - -
- - -
         ::= 'SYSTEM' ( 'ARCHIVE' 'LOG' | 'CHECKPOINT' | 'DUMP' 'ACTIVE' 'SESSION' - 'HISTORY' | ( 'ENABLE' | 'DISABLE' ) ( 'DISTRIBUTED' 'RECOVERY' | 'RESTRICTED' 'SESSION' - ) | 'FLUSH' | ( 'DISCONNECT' | 'KILL' ) 'SESSION' | 'SWITCH' | 'SUSPEND' | 'RESUME' - | 'QUIESCE' 'RESTRICTED' | 'UNQIESCE' | 'SHUTDOWN' | 'REGISTER' | 'SET' | 'RESET' - ) captureRest
-
- Referenced by: -
- - -====================================================================================================================== -Wait -====================================================================================================================== - - -.. raw:: html - - - - - - WAIT - - S_LONG - -
- -
Wait     ::= 'WAIT' S_LONG
-
- Referenced by: -
- - -====================================================================================================================== -SavepointStatement -====================================================================================================================== - - -.. raw:: html - - - - - - SAVEPOINT - - S_IDENTIFIER - -
- - -
         ::= 'SAVEPOINT' S_IDENTIFIER
-
- Referenced by: -
- - -====================================================================================================================== -RollbackStatement -====================================================================================================================== - - -.. raw:: html - - - - - - ROLLBACK - - WORK - - TO - - SAVEPOINT - - S_IDENTIFIER - FORCE - - S_CHAR_LITERAL - -
- - -
         ::= 'ROLLBACK' 'WORK'? ( 'TO' 'SAVEPOINT'? S_IDENTIFIER | 'FORCE' S_CHAR_LITERAL )?
-
- Referenced by: -
- - -====================================================================================================================== -Comment -====================================================================================================================== - - -.. raw:: html - - - - - - COMMENT - - ON - - TABLE - - VIEW - - Table - COLUMN - - Column - IS - - S_CHAR_LITERAL - -
- -
Comment  ::= 'COMMENT' 'ON' ( ( 'TABLE' | 'VIEW' ) Table | 'COLUMN' Column ) 'IS' S_CHAR_LITERAL
-
- Referenced by: -
- - -====================================================================================================================== -Grant -====================================================================================================================== - - -.. raw:: html - - - - - - GRANT - - readGrantTypes - , - - ON - - RelObjectNames - - S_IDENTIFIER - TO - - UsersList - -
- -
Grant    ::= 'GRANT' ( ( readGrantTypes ( ',' readGrantTypes )* )? 'ON' RelObjectNames | S_IDENTIFIER ) 'TO' UsersList
-
- Referenced by: -
- - -====================================================================================================================== -UsersList -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectName - , - - ColumnsNamesListItem - -
- - -
         ::= RelObjectName ( ',' ColumnsNamesListItem )*
-
- Referenced by: -
- - -====================================================================================================================== -readGrantTypes -====================================================================================================================== - - -.. raw:: html - - - - - - K_SELECT - INSERT - - UPDATE - - DELETE - - EXECUTE - - ALTER - - DROP - - -
- - -
         ::= K_SELECT
-
           | 'INSERT'
-
           | 'UPDATE'
-
           | 'DELETE'
-
           | 'EXECUTE'
-
           | 'ALTER'
-
           | 'DROP'
-
- Referenced by: -
- - -====================================================================================================================== -Sequence -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNames - -
- - -
- Referenced by: -
- - -====================================================================================================================== -SequenceParameters -====================================================================================================================== - - -.. raw:: html - - - - - - INCREMENT - - BY - - START - - WITH - - MAXVALUE - - MINVALUE - - CACHE - - S_LONG - RESTART - - WITH - - S_LONG - NOMAXVALUE - - NOMINVALUE - - NOCYCLE - - CYCLE - - NOCACHE - - ORDER - - NOORDER - - KEEP - - NOKEEP - - SESSION - - GLOBAL - - -
- - -
         ::= ( ( 'INCREMENT' 'BY'? | 'START' 'WITH'? | 'MAXVALUE' | 'MINVALUE' | 'CACHE' - ) S_LONG | 'RESTART' ( 'WITH' S_LONG )? | 'NOMAXVALUE' | 'NOMINVALUE' | 'NOCYCLE' | 'CYCLE' | 'NOCACHE' | 'ORDER' | 'NOORDER' - | 'KEEP' | 'NOKEEP' | 'SESSION' | 'GLOBAL' )*
-
- Referenced by: -
- - -====================================================================================================================== -CreateSequence -====================================================================================================================== - - -.. raw:: html - - - - - - SEQUENCE - - Sequence - AS - - S_IDENTIFIER - - DATA_TYPE - - SequenceParameters - -
- - -
         ::= 'SEQUENCE' Sequence ( 'AS' ( S_IDENTIFIER | DATA_TYPE ) )? SequenceParameters
-
- Referenced by: -
- - -====================================================================================================================== -AlterSequence -====================================================================================================================== - - -.. raw:: html - - - - - - SEQUENCE - - Sequence - - SequenceParameters - -
- - -
         ::= 'SEQUENCE' Sequence SequenceParameters
-
- Referenced by: -
- - -====================================================================================================================== -Create -====================================================================================================================== - - -.. raw:: html - - - - - - CREATE - - OR - - REPLACE - - CreateFunctionStatement - - CreateSchema - - CreateSequence - - CreateSynonym - - CreateTable - - CreateView - - CreatePolicy - TRIGGER - - DOMAIN - - captureRest - - CreateIndex - -
- -
Create   ::= 'CREATE' ( 'OR' 'REPLACE' )? ( CreateFunctionStatement | CreateSchema | CreateSequence | CreateSynonym | CreateTable | CreateView | CreatePolicy | ( 'TRIGGER' | 'DOMAIN' )? captureRest | CreateIndex )
-
- Referenced by: -
- - -====================================================================================================================== -CreateFunctionStatement -====================================================================================================================== - - -.. raw:: html - - - - - - FUNCTION - - PROCEDURE - - captureFunctionBody - -
- - -
         ::= ( 'FUNCTION' | 'PROCEDURE' ) captureFunctionBody
-
- Referenced by: -
- - -====================================================================================================================== -CreateSynonym -====================================================================================================================== - - -.. raw:: html - - - - - - PUBLIC - - SYNONYM - - Synonym - FOR - - RelObjectNames - -
- - -
         ::= 'PUBLIC'? 'SYNONYM' Synonym 'FOR' RelObjectNames
-
- Referenced by: -
- - -====================================================================================================================== -Synonym -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNames - -
- - -
- Referenced by: -
- - -====================================================================================================================== -CreatePolicy -====================================================================================================================== - - -.. raw:: html - - - - - - POLICY - - RelObjectName - ON - - Table - FOR - - ALL - - K_SELECT - INSERT - - UPDATE - - DELETE - - TO - - RelObjectName - , - - USING - - ( - - Expression - ) - - WITH - - CHECK - - ( - - Expression - ) - - -
- - -
         ::= 'POLICY' RelObjectName 'ON' Table ( 'FOR' ( 'ALL' | K_SELECT | 'INSERT' | 'UPDATE' | 'DELETE' ) )? ( 'TO' RelObjectName ( ',' RelObjectName )* )? ( 'USING' '(' Expression ')' )? ( 'WITH' 'CHECK' '(' Expression ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -UnsupportedStatement -====================================================================================================================== - - -.. raw:: html - - - - - - captureUnsupportedStatementDeclaration - -
- - - -
- Referenced by: -
- - -====================================================================================================================== -IdentifierChain -====================================================================================================================== - - -.. raw:: html - - - - - - RelObjectNameExt - . - - -
- - -
         ::= RelObjectNameExt ( '.' RelObjectNameExt )*
-
- - -====================================================================================================================== -IdentifierChain2 -====================================================================================================================== - - -.. raw:: html - - - - - - . - - RelObjectNameExt - -
- - -
         ::= ( '.' RelObjectNameExt )*
-
- Referenced by: -
- - -====================================================================================================================== -CharacterPrimary -====================================================================================================================== - - -.. raw:: html - - - - - - TranscodingFunction - - TrimFunction - -
- - -
         ::= TranscodingFunction
-
           | TrimFunction
-
- Referenced by: -
- - -====================================================================================================================== -TranscodingFunction -====================================================================================================================== - - -.. raw:: html - - - - - - TRY_CONVERT - - SAFE_CONVERT - - CONVERT - - ( - - ColDataType - , - - Expression - , - - S_LONG - - Expression - USING - - IdentifierChain - ) - - -
- - -
         ::= ( 'TRY_CONVERT' | 'SAFE_CONVERT' | 'CONVERT' ) '(' ( ColDataType ',' Expression ( ',' S_LONG )? | Expression 'USING' IdentifierChain ) ')'
-
- Referenced by: -
- - -====================================================================================================================== -TrimFunction -====================================================================================================================== - - -.. raw:: html - - - - - - TRIM - - ( - - LEADING - - TRAILING - - BOTH - - Expression - , - - FROM - - Expression - ) - - -
- - -
         ::= 'TRIM' '(' ( 'LEADING' | 'TRAILING' | 'BOTH' )? Expression? ( ( ',' | 'FROM' ) Expression )? ')'
-
- Referenced by: -
- - -====================================================================================================================== -SnowflakeTimeTravelAt -====================================================================================================================== - - -.. raw:: html - - - - - - AT - - ( - - K_DATETIMELITERAL - OFFSET - - => - - Expression - STATEMENT - - => - - S_CHAR_LITERAL - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - STREAM - - => - - S_CHAR_LITERAL - ) - - -
- - -
         ::= 'AT' '(' ( ( K_DATETIMELITERAL | 'OFFSET' ) '=>' Expression | 'STATEMENT' '=>' ( S_CHAR_LITERAL | S_IDENTIFIER | S_QUOTED_IDENTIFIER ) | 'STREAM' '=>' S_CHAR_LITERAL ) ')'
-
- - -====================================================================================================================== -SnowflakeTimeTravelBefore -====================================================================================================================== - - -.. raw:: html - - - - - - BEFORE - - ( - - STATEMENT - - => - - S_CHAR_LITERAL - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - ) - - -
- - -
         ::= 'BEFORE' '(' 'STATEMENT' '=>' ( S_CHAR_LITERAL | S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ')'
-
- - -====================================================================================================================== -SnowflakeTimeTravelChange -====================================================================================================================== - - -.. raw:: html - - - - - - CHANGES - - ( - - INFORMATION - - => - - DEFAULT - - APPEND_ONLY - - ) - - SnowflakeTimeTravelAt - - SnowflakeTimeTravelBefore - END - - ( - - K_DATETIMELITERAL - OFFSET - - => - - Expression - STATEMENT - - => - - S_CHAR_LITERAL - - S_IDENTIFIER - - S_QUOTED_IDENTIFIER - ) - - -
- - -
         ::= 'CHANGES' '(' 'INFORMATION' '=>' ( 'DEFAULT' | 'APPEND_ONLY' ) ')' ( - SnowflakeTimeTravelAt | SnowflakeTimeTravelBefore ) ( 'END' '(' ( ( K_DATETIMELITERAL | 'OFFSET' ) '=>' Expression | 'STATEMENT' '=>' ( S_CHAR_LITERAL | S_IDENTIFIER | S_QUOTED_IDENTIFIER ) ) ')' )?
-
- Referenced by: -
- - -====================================================================================================================== -DataBricksTemporalSpec -====================================================================================================================== - - -.. raw:: html - - - - - - @ - - @V - - S_CHAR_LITERAL - - S_LONG - FOR - - SYSTEM_TIMESTAMP - - K_DATETIMELITERAL - AS - - OF - - Expression - SYSTEM_VERSION - - VERSION - - AS - - OF - - S_LONG - - S_CHAR_LITERAL - -
- - -
         ::= ( '@' | '@V' ) ( S_CHAR_LITERAL | S_LONG )
-
           | 'FOR'? ( 'SYSTEM_TIMESTAMP' | K_DATETIMELITERAL ) 'AS' 'OF' Expression
-
           | ( 'SYSTEM_VERSION' | 'VERSION' ) 'AS' 'OF' ( S_LONG | S_CHAR_LITERAL )
-
- Referenced by: -
- - -====================================================================================================================== -BigQueryHistoricalVersion -====================================================================================================================== - - -.. raw:: html - - - - - - FOR - - SYSTEM_TIME - - AS - - OF - - Expression - -
- - -
         ::= 'FOR' 'SYSTEM_TIME' 'AS' 'OF' Expression
-
- Referenced by: -
- - -====================================================================================================================== -TimeTravelBeforeAlias -====================================================================================================================== - - -.. raw:: html - - - - - - SnowflakeTimeTravelAt - - SnowflakeTimeTravelBefore - - SnowflakeTimeTravelChange - - DataBricksTemporalSpec - -
- - -
         ::= SnowflakeTimeTravelAt
-
           | SnowflakeTimeTravelBefore
-
           | SnowflakeTimeTravelChange
-
           | DataBricksTemporalSpec
-
- Referenced by: -
- - -====================================================================================================================== -TimeTravelAfterAlias -====================================================================================================================== - - -.. raw:: html - - - - - - BigQueryHistoricalVersion - -
- - -
         ::= BigQueryHistoricalVersion
-
- Referenced by: -
- - -====================================================================================================================== -WHITESPACE -====================================================================================================================== - - -.. raw:: html - - - - - - - - [#x9] - - [#xD] - - [#xA] - - -
- - -
         ::= [ #x9#xD#xA]
-
- - -====================================================================================================================== -K_ISOLATION -====================================================================================================================== - - -.. raw:: html - - - - - - UR - - RS - - RR - - CS - - -
- - -
         ::= 'UR'
-
           | 'RS'
-
           | 'RR'
-
           | 'CS'
-
- Referenced by: -
- - -====================================================================================================================== -K_NEXTVAL -====================================================================================================================== - - -.. raw:: html - - - - - - NEXTVAL - - - - FOR - - NEXT - - - - VALUE - - - - FOR - - -
- - -
         ::= 'NEXTVAL' ( ' '+ 'FOR' )?
-
           | 'NEXT' ' '+ 'VALUE' ' '+ 'FOR'
-
- - -====================================================================================================================== -K_TEXT_LITERAL -====================================================================================================================== - - -.. raw:: html - - - - - - TEXT - - TINYTEXT - - MEDIUMTEXT - - LONGTEXT - - -
- - -
         ::= 'TEXT'
-
           | 'TINYTEXT'
-
           | 'MEDIUMTEXT'
-
           | 'LONGTEXT'
-
- Referenced by: -
- - -====================================================================================================================== -K_TIME_KEY_EXPR -====================================================================================================================== - - -.. raw:: html - - - - - - CURRENT - - _ - - - - TIMESTAMP - - TIME - - DATE - - TIMEZONE - - () - - -
- - -
         ::= 'CURRENT' ( '_' | ' '+ ) ( 'TIMESTAMP' | 'TIME' | 'DATE' | 'TIMEZONE' - ) '()'?
-
- - -====================================================================================================================== -K_STRING_FUNCTION_NAME -====================================================================================================================== - - -.. raw:: html - - - - - - SUBSTR - - SUBSTRING - - TRIM - - POSITION - - OVERLAY - - -
- - -
         ::= 'SUBSTR'
-
           | 'SUBSTRING'
-
           | 'TRIM'
-
           | 'POSITION'
-
           | 'OVERLAY'
-
- - -====================================================================================================================== -K_DATETIMELITERAL -====================================================================================================================== - - -.. raw:: html - - - - - - DATE - - DATETIME - - TIME - - TIMESTAMP - - TIMESTAMPTZ - - -
- - -
         ::= 'DATE'
-
           | 'DATETIME'
-
           | 'TIME'
-
           | 'TIMESTAMP'
-
           | 'TIMESTAMPTZ'
-
- - -====================================================================================================================== -K_DATE_LITERAL -====================================================================================================================== - - -.. raw:: html - - - - - - YEAR - - MONTH - - DAY - - HOUR - - MINUTE - - SECOND - - -
- - -
         ::= 'YEAR'
-
           | 'MONTH'
-
           | 'DAY'
-
           | 'HOUR'
-
           | 'MINUTE'
-
           | 'SECOND'
-
- - -====================================================================================================================== -K_SELECT -====================================================================================================================== - - -.. raw:: html - - - - - - SELECT - - SEL - - -
- -
K_SELECT ::= 'SELECT'
-
           | 'SEL'
-
- - -====================================================================================================================== -K_SIMILAR_TO -====================================================================================================================== - - -.. raw:: html - - - - - - SIMILAR - - - - TO - - -
- - -
         ::= 'SIMILAR' ' '+ 'TO'
-
- Referenced by: -
- - -====================================================================================================================== -ST_SEMICOLON -====================================================================================================================== - - -.. raw:: html - - - - - - ; - - [#xA] - - / - - [#xA] - - go - - [#xA] - - -
- - -
         ::= ';'
-
           | #xA ( [/#xA] | 'go' ) #xA
-
- Referenced by: -
- - -====================================================================================================================== -OP_GREATERTHANEQUALS -====================================================================================================================== - - -.. raw:: html - - - - - - > - - WHITESPACE - = - - -
- - -
         ::= '>' WHITESPACE* '='
-
- Referenced by: -
- - -====================================================================================================================== -OP_MINORTHANEQUALS -====================================================================================================================== - - -.. raw:: html - - - - - - < - - WHITESPACE - = - - -
- - -
         ::= '<' WHITESPACE* '='
-
- Referenced by: -
- - -====================================================================================================================== -OP_NOTEQUALSSTANDARD -====================================================================================================================== - - -.. raw:: html - - - - - - < - - WHITESPACE - > - - -
- - -
         ::= '<' WHITESPACE* '>'
-
- Referenced by: -
- - -====================================================================================================================== -OP_NOTEQUALSBANG -====================================================================================================================== - - -.. raw:: html - - - - - - ! - - WHITESPACE - = - - -
- - -
         ::= '!' WHITESPACE* '='
-
- Referenced by: -
- - -====================================================================================================================== -DT_ZONE -====================================================================================================================== - - -.. raw:: html - - - - - - K_DATETIMELITERAL - - WHITESPACE - ( - - S_LONG - ) - - WHITESPACE - WITH - - WITHOUT - - WHITESPACE - LOCAL - - WHITESPACE - TIME - - WHITESPACE - ZONE - - -
- -
DT_ZONE  ::= K_DATETIMELITERAL WHITESPACE* ( '(' S_LONG ')' )? WHITESPACE* ( 'WITH' | 'WITHOUT' ) WHITESPACE+ ( 'LOCAL' WHITESPACE+ )? 'TIME' WHITESPACE+ 'ZONE'
-
- - -====================================================================================================================== -DATA_TYPE -====================================================================================================================== - - -.. raw:: html - - - - - - BISTRING - - TYPE_BLOB - - TYPE_BOOLEAN - ENUM - - TYPE_REAL - - TYPE_DOUBLE - UUID - - MAP - - TYPE_TINYINT - - TYPE_SMALLINT - - TYPE_INTEGER - - TYPE_BIGINT - HUGEINT - - UTINYINT - - USMALLINT - - UINTEGER - - UBIGINT - - UHUGEINT - - TYPE_DECIMAL - - TYPE_VARCHAR - TIMETZ - - TYPE_TIMESTAMP - -
- - -
         ::= 'BISTRING'
-
           | TYPE_BLOB
-
           | TYPE_BOOLEAN
-
           | 'ENUM'
-
           | TYPE_REAL
-
           | TYPE_DOUBLE
-
           | 'UUID'
-
           | 'MAP'
-
           | TYPE_TINYINT
-
           | TYPE_SMALLINT
-
           | TYPE_INTEGER
-
           | TYPE_BIGINT
-
           | 'HUGEINT'
-
           | 'UTINYINT'
-
           | 'USMALLINT'
-
           | 'UINTEGER'
-
           | 'UBIGINT'
-
           | 'UHUGEINT'
-
           | TYPE_DECIMAL
-
           | TYPE_VARCHAR
-
           | 'TIMETZ'
-
           | TYPE_TIMESTAMP
-
- - -====================================================================================================================== -TYPE_BLOB -====================================================================================================================== - - -.. raw:: html - - - - - - BLOB - - BYTEA - - BINARY - - VARBINARY - - BYTES - - -
- - -
         ::= 'BLOB'
-
           | 'BYTEA'
-
           | 'BINARY'
-
           | 'VARBINARY'
-
           | 'BYTES'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_BOOLEAN -====================================================================================================================== - - -.. raw:: html - - - - - - BOOLEAN - - BOOL - - -
- - -
         ::= 'BOOLEAN'
-
           | 'BOOL'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_DECIMAL -====================================================================================================================== - - -.. raw:: html - - - - - - DECIMAL - - NUMBER - - NUMERIC - - -
- - -
         ::= 'DECIMAL'
-
           | 'NUMBER'
-
           | 'NUMERIC'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_TINYINT -====================================================================================================================== - - -.. raw:: html - - - - - - TINYINT - - INT1 - - -
- - -
         ::= 'TINYINT'
-
           | 'INT1'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_SMALLINT -====================================================================================================================== - - -.. raw:: html - - - - - - SMALLINT - - INT2 - - SHORT - - -
- - -
         ::= 'SMALLINT'
-
           | 'INT2'
-
           | 'SHORT'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_INTEGER -====================================================================================================================== - - -.. raw:: html - - - - - - INTEGER - - INT - - INT4 - - SIGNED - - UNSIGNED - - -
- - -
         ::= 'INTEGER'
-
           | 'INT'
-
           | 'INT4'
-
           | 'SIGNED'
-
           | 'UNSIGNED'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_BIGINT -====================================================================================================================== - - -.. raw:: html - - - - - - BIGINT - - INT8 - - LONG - - -
- - -
         ::= 'BIGINT'
-
           | 'INT8'
-
           | 'LONG'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_REAL -====================================================================================================================== - - -.. raw:: html - - - - - - REAL - - FLOAT4 - - FLOAT - - -
- - -
         ::= 'REAL'
-
           | 'FLOAT4'
-
           | 'FLOAT'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_DOUBLE -====================================================================================================================== - - -.. raw:: html - - - - - - DOUBLE - - PRECISION - - FLOAT8 - - FLOAT64 - - -
- - -
         ::= 'DOUBLE'
-
           | 'PRECISION'
-
           | 'FLOAT8'
-
           | 'FLOAT64'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_VARCHAR -====================================================================================================================== - - -.. raw:: html - - - - - - NVARCHAR - - VARCHAR - - NCHAR - - CHAR - - BPCHAR - - TEXT - - STRING - - CHARACTER - - VARYING - - -
- - -
         ::= 'NVARCHAR'
-
           | 'VARCHAR'
-
           | 'NCHAR'
-
           | 'CHAR'
-
           | 'BPCHAR'
-
           | 'TEXT'
-
           | 'STRING'
-
           | 'CHARACTER'
-
           | 'VARYING'
-
- Referenced by: -
- - -====================================================================================================================== -TYPE_TIMESTAMP -====================================================================================================================== - - -.. raw:: html - - - - - - TIMESTAMP_NS - - TIMESTAMP_MS - - TIMESTAMP_S - - -
- - -
         ::= 'TIMESTAMP_NS'
-
           | 'TIMESTAMP_MS'
-
           | 'TIMESTAMP_S'
-
- Referenced by: -
- - -====================================================================================================================== -S_DOUBLE -====================================================================================================================== - - -.. raw:: html - - - - - - S_LONG - . - - S_LONG - e - - E - - + - - [#x2D] - - S_LONG - - S_LONG - . - - e - - E - - + - - [#x2D] - - S_LONG - e - - E - - + - - [#x2D] - - S_LONG - -
- -
S_DOUBLE ::= S_LONG? '.' S_LONG ( [eE] [+#x2D]? S_LONG )?
-
           | S_LONG ( '.' ( [eE] [+#x2D]? S_LONG )? | [eE] [+#x2D]? S_LONG )
-
- - -====================================================================================================================== -S_LONG -====================================================================================================================== - - -.. raw:: html - - - - - - DIGIT - -
- -
S_LONG   ::= DIGIT+
-
- - -====================================================================================================================== -DIGIT -====================================================================================================================== - - -.. raw:: html - - - - - - [0-9] - - -
- -
DIGIT    ::= [0-9]
-
- Referenced by: -
- - -====================================================================================================================== -S_HEX -====================================================================================================================== - - -.. raw:: html - - - - - - X - - ' - - HEX_VALUE - ' - - - - 0x - - HEX_VALUE - -
- -
S_HEX    ::= 'X' ( "'" HEX_VALUE* "'" ' '* )+
-
           | '0x' HEX_VALUE+
-
- - -====================================================================================================================== -HEX_VALUE -====================================================================================================================== - - -.. raw:: html - - - - - - [0-9] - - [A-F] - - - - -
- - -
         ::= [0-9A-F ]
-
- Referenced by: -
- - -====================================================================================================================== -LINE_COMMENT -====================================================================================================================== - - -.. raw:: html - - - - - - -- - - // - - [^#xD#xA] - - -
- - -
         ::= ( '--' | '//' ) [^#xD#xA]*
-
- Not referenced by any. -
- - -====================================================================================================================== -MULTI_LINE_COMMENT -====================================================================================================================== - - -.. raw:: html - - - - - - . - - -
- - -
         ::= .
-
- Not referenced by any. -
- - -====================================================================================================================== -S_PARAMETER -====================================================================================================================== - - -.. raw:: html - - - - - - $ - - [0-9] - - -
- - -
         ::= '$' [0-9]+
-
- Referenced by: -
- - -====================================================================================================================== -S_IDENTIFIER -====================================================================================================================== - - -.. raw:: html - - - - - - LETTER - - PART_LETTER - $ - - PART_LETTER_NO_DOLLAR - - PART_LETTER - -
- - -
         ::= LETTER PART_LETTER*
-
           | '$' ( PART_LETTER_NO_DOLLAR PART_LETTER* )?
-
- - -====================================================================================================================== -LETTER -====================================================================================================================== - - -.. raw:: html - - - - - - UnicodeIdentifierStart - - Nd - _ - - [#x23] - - -
- - -
           | Nd
-
           | [_#x23]
-
- Referenced by: -
- - -====================================================================================================================== -PART_LETTER_NO_DOLLAR -====================================================================================================================== - - -.. raw:: html - - - - - - UnicodeIdentifierStart - - UnicodeIdentifierExtend - _ - - @ - - [#x23] - - -
- - -
         ::= UnicodeIdentifierStart
-
           | UnicodeIdentifierExtend
-
           | [_@#x23]
-
- Referenced by: -
- - -====================================================================================================================== -PART_LETTER -====================================================================================================================== - - -.. raw:: html - - - - - - UnicodeIdentifierStart - - UnicodeIdentifierExtend - $ - - _ - - @ - - [#x23] - - -
- - -
         ::= UnicodeIdentifierStart
-
           | UnicodeIdentifierExtend
-
           | [$_@#x23]
-
- Referenced by: -
- - -====================================================================================================================== -S_AT_IDENTIFIER -====================================================================================================================== - - -.. raw:: html - - - - - - @ - - @ - - S_IDENTIFIER - -
- - -
         ::= '@' '@'? S_IDENTIFIER
-
- Referenced by: -
- - -====================================================================================================================== -UnicodeIdentifierStart -====================================================================================================================== - - -.. raw:: html - - - - - - [#xB7] - - Ll - - Lm - - Lo - - Lt - - Lu - - Nl - - CJK - -
- - -
         ::= #xB7
-
           | Ll
-
           | Lm
-
           | Lo
-
           | Lt
-
           | Lu
-
           | Nl
-
           | CJK
-
- - -====================================================================================================================== -Ll -====================================================================================================================== - - -.. raw:: html - - - - - - [a-z] - - [#xB5] - - [#xDF-#xF6] - - [#xF8-#xFF] - - [#x101] - - [#x103] - - [#x105] - - [#x107] - - [#x109] - - [#x10B] - - [#x10D] - - [#x10F] - - [#x111] - - [#x113] - - [#x115] - - [#x117] - - [#x119] - - [#x11B] - - [#x11D] - - [#x11F] - - [#x121] - - [#x123] - - [#x125] - - [#x127] - - [#x129] - - [#x12B] - - [#x12D] - - [#x12F] - - [#x131] - - [#x133] - - [#x135] - - [#x137-#x138] - - [#x13A] - - [#x13C] - - [#x13E] - - [#x140] - - [#x142] - - [#x144] - - [#x146] - - [#x148-#x149] - - [#x14B] - - [#x14D] - - [#x14F] - - [#x151] - - [#x153] - - [#x155] - - [#x157] - - [#x159] - - [#x15B] - - [#x15D] - - [#x15F] - - [#x161] - - [#x163] - - [#x165] - - [#x167] - - [#x169] - - [#x16B] - - [#x16D] - - [#x16F] - - [#x171] - - [#x173] - - [#x175] - - [#x177] - - [#x17A] - - [#x17C] - - [#x17E-#x180] - - [#x183] - - [#x185] - - [#x188] - - [#x18C-#x18D] - - [#x192] - - [#x195] - - [#x199-#x19B] - - [#x19E] - - [#x1A1] - - [#x1A3] - - [#x1A5] - - [#x1A8] - - [#x1AA-#x1AB] - - [#x1AD] - - [#x1B0] - - [#x1B4] - - [#x1B6] - - [#x1B9-#x1BA] - - [#x1BD-#x1BF] - - [#x1C6] - - [#x1C9] - - [#x1CC] - - [#x1CE] - - [#x1D0] - - [#x1D2] - - [#x1D4] - - [#x1D6] - - [#x1D8] - - [#x1DA] - - [#x1DC-#x1DD] - - [#x1DF] - - [#x1E1] - - [#x1E3] - - [#x1E5] - - [#x1E7] - - [#x1E9] - - [#x1EB] - - [#x1ED] - - [#x1EF-#x1F0] - - [#x1F3] - - [#x1F5] - - [#x1F9] - - [#x1FB] - - [#x1FD] - - [#x1FF] - - [#x201] - - [#x203] - - [#x205] - - [#x207] - - [#x209] - - [#x20B] - - [#x20D] - - [#x20F] - - [#x211] - - [#x213] - - [#x215] - - [#x217] - - [#x219] - - [#x21B] - - [#x21D] - - [#x21F] - - [#x221] - - [#x223] - - [#x225] - - [#x227] - - [#x229] - - [#x22B] - - [#x22D] - - [#x22F] - - [#x231] - - [#x233-#x239] - - [#x23C] - - [#x23F-#x240] - - [#x242] - - [#x247] - - [#x249] - - [#x24B] - - [#x24D] - - [#x24F-#x293] - - [#x295-#x2AF] - - [#x371] - - [#x373] - - [#x377] - - [#x37B-#x37D] - - [#x390] - - [#x3AC-#x3CE] - - [#x3D0-#x3D1] - - [#x3D5-#x3D7] - - [#x3D9] - - [#x3DB] - - [#x3DD] - - [#x3DF] - - [#x3E1] - - [#x3E3] - - [#x3E5] - - [#x3E7] - - [#x3E9] - - [#x3EB] - - [#x3ED] - - [#x3EF-#x3F3] - - [#x3F5] - - [#x3F8] - - [#x3FB-#x3FC] - - [#x430-#x45F] - - [#x461] - - [#x463] - - [#x465] - - [#x467] - - [#x469] - - [#x46B] - - [#x46D] - - [#x46F] - - [#x471] - - [#x473] - - [#x475] - - [#x477] - - [#x479] - - [#x47B] - - [#x47D] - - [#x47F] - - [#x481] - - [#x48B] - - [#x48D] - - [#x48F] - - [#x491] - - [#x493] - - [#x495] - - [#x497] - - [#x499] - - [#x49B] - - [#x49D] - - [#x49F] - - [#x4A1] - - [#x4A3] - - [#x4A5] - - [#x4A7] - - [#x4A9] - - [#x4AB] - - [#x4AD] - - [#x4AF] - - [#x4B1] - - [#x4B3] - - [#x4B5] - - [#x4B7] - - [#x4B9] - - [#x4BB] - - [#x4BD] - - [#x4BF] - - [#x4C2] - - [#x4C4] - - [#x4C6] - - [#x4C8] - - [#x4CA] - - [#x4CC] - - [#x4CE-#x4CF] - - [#x4D1] - - [#x4D3] - - [#x4D5] - - [#x4D7] - - [#x4D9] - - [#x4DB] - - [#x4DD] - - [#x4DF] - - [#x4E1] - - [#x4E3] - - [#x4E5] - - [#x4E7] - - [#x4E9] - - [#x4EB] - - [#x4ED] - - [#x4EF] - - [#x4F1] - - [#x4F3] - - [#x4F5] - - [#x4F7] - - [#x4F9] - - [#x4FB] - - [#x4FD] - - [#x4FF] - - [#x501] - - [#x503] - - [#x505] - - [#x507] - - [#x509] - - [#x50B] - - [#x50D] - - [#x50F] - - [#x511] - - [#x513] - - [#x515] - - [#x517] - - [#x519] - - [#x51B] - - [#x51D] - - [#x51F] - - [#x521] - - [#x523] - - [#x525] - - [#x527] - - [#x529] - - [#x52B] - - [#x52D] - - [#x52F] - - [#x560-#x588] - - [#x10D0-#x10FA] - - [#x10FD-#x10FF] - - [#x13F8-#x13FD] - - [#x1C80-#x1C88] - - [#x1D00-#x1D2B] - - [#x1D6B-#x1D77] - - [#x1D79-#x1D9A] - - [#x1E01] - - [#x1E03] - - [#x1E05] - - [#x1E07] - - [#x1E09] - - [#x1E0B] - - [#x1E0D] - - [#x1E0F] - - [#x1E11] - - [#x1E13] - - [#x1E15] - - [#x1E17] - - [#x1E19] - - [#x1E1B] - - [#x1E1D] - - [#x1E1F] - - [#x1E21] - - [#x1E23] - - [#x1E25] - - [#x1E27] - - [#x1E29] - - [#x1E2B] - - [#x1E2D] - - [#x1E2F] - - [#x1E31] - - [#x1E33] - - [#x1E35] - - [#x1E37] - - [#x1E39] - - [#x1E3B] - - [#x1E3D] - - [#x1E3F] - - [#x1E41] - - [#x1E43] - - [#x1E45] - - [#x1E47] - - [#x1E49] - - [#x1E4B] - - [#x1E4D] - - [#x1E4F] - - [#x1E51] - - [#x1E53] - - [#x1E55] - - [#x1E57] - - [#x1E59] - - [#x1E5B] - - [#x1E5D] - - [#x1E5F] - - [#x1E61] - - [#x1E63] - - [#x1E65] - - [#x1E67] - - [#x1E69] - - [#x1E6B] - - [#x1E6D] - - [#x1E6F] - - [#x1E71] - - [#x1E73] - - [#x1E75] - - [#x1E77] - - [#x1E79] - - [#x1E7B] - - [#x1E7D] - - [#x1E7F] - - [#x1E81] - - [#x1E83] - - [#x1E85] - - [#x1E87] - - [#x1E89] - - [#x1E8B] - - [#x1E8D] - - [#x1E8F] - - [#x1E91] - - [#x1E93] - - [#x1E95-#x1E9D] - - [#x1E9F] - - [#x1EA1] - - [#x1EA3] - - [#x1EA5] - - [#x1EA7] - - [#x1EA9] - - [#x1EAB] - - [#x1EAD] - - [#x1EAF] - - [#x1EB1] - - [#x1EB3] - - [#x1EB5] - - [#x1EB7] - - [#x1EB9] - - [#x1EBB] - - [#x1EBD] - - [#x1EBF] - - [#x1EC1] - - [#x1EC3] - - [#x1EC5] - - [#x1EC7] - - [#x1EC9] - - [#x1ECB] - - [#x1ECD] - - [#x1ECF] - - [#x1ED1] - - [#x1ED3] - - [#x1ED5] - - [#x1ED7] - - [#x1ED9] - - [#x1EDB] - - [#x1EDD] - - [#x1EDF] - - [#x1EE1] - - [#x1EE3] - - [#x1EE5] - - [#x1EE7] - - [#x1EE9] - - [#x1EEB] - - [#x1EED] - - [#x1EEF] - - [#x1EF1] - - [#x1EF3] - - [#x1EF5] - - [#x1EF7] - - [#x1EF9] - - [#x1EFB] - - [#x1EFD] - - [#x1EFF-#x1F07] - - [#x1F10-#x1F15] - - [#x1F20-#x1F27] - - [#x1F30-#x1F37] - - [#x1F40-#x1F45] - - [#x1F50-#x1F57] - - [#x1F60-#x1F67] - - [#x1F70-#x1F7D] - - [#x1F80-#x1F87] - - [#x1F90-#x1F97] - - [#x1FA0-#x1FA7] - - [#x1FB0-#x1FB4] - - [#x1FB6-#x1FB7] - - [#x1FBE] - - [#x1FC2-#x1FC4] - - [#x1FC6-#x1FC7] - - [#x1FD0-#x1FD3] - - [#x1FD6-#x1FD7] - - [#x1FE0-#x1FE7] - - [#x1FF2-#x1FF4] - - [#x1FF6-#x1FF7] - - [#x210A] - - [#x210E-#x210F] - - [#x2113] - - [#x212F] - - [#x2134] - - [#x2139] - - [#x213C-#x213D] - - [#x2146-#x2149] - - [#x214E] - - [#x2184] - - [#x2C30-#x2C5F] - - [#x2C61] - - [#x2C65-#x2C66] - - [#x2C68] - - [#x2C6A] - - [#x2C6C] - - [#x2C71] - - [#x2C73-#x2C74] - - [#x2C76-#x2C7B] - - [#x2C81] - - [#x2C83] - - [#x2C85] - - [#x2C87] - - [#x2C89] - - [#x2C8B] - - [#x2C8D] - - [#x2C8F] - - [#x2C91] - - [#x2C93] - - [#x2C95] - - [#x2C97] - - [#x2C99] - - [#x2C9B] - - [#x2C9D] - - [#x2C9F] - - [#x2CA1] - - [#x2CA3] - - [#x2CA5] - - [#x2CA7] - - [#x2CA9] - - [#x2CAB] - - [#x2CAD] - - [#x2CAF] - - [#x2CB1] - - [#x2CB3] - - [#x2CB5] - - [#x2CB7] - - [#x2CB9] - - [#x2CBB] - - [#x2CBD] - - [#x2CBF] - - [#x2CC1] - - [#x2CC3] - - [#x2CC5] - - [#x2CC7] - - [#x2CC9] - - [#x2CCB] - - [#x2CCD] - - [#x2CCF] - - [#x2CD1] - - [#x2CD3] - - [#x2CD5] - - [#x2CD7] - - [#x2CD9] - - [#x2CDB] - - [#x2CDD] - - [#x2CDF] - - [#x2CE1] - - [#x2CE3-#x2CE4] - - [#x2CEC] - - [#x2CEE] - - [#x2CF3] - - [#x2D00-#x2D25] - - [#x2D27] - - [#x2D2D] - - [#xA641] - - [#xA643] - - [#xA645] - - [#xA647] - - [#xA649] - - [#xA64B] - - [#xA64D] - - [#xA64F] - - [#xA651] - - [#xA653] - - [#xA655] - - [#xA657] - - [#xA659] - - [#xA65B] - - [#xA65D] - - [#xA65F] - - [#xA661] - - [#xA663] - - [#xA665] - - [#xA667] - - [#xA669] - - [#xA66B] - - [#xA66D] - - [#xA681] - - [#xA683] - - [#xA685] - - [#xA687] - - [#xA689] - - [#xA68B] - - [#xA68D] - - [#xA68F] - - [#xA691] - - [#xA693] - - [#xA695] - - [#xA697] - - [#xA699] - - [#xA69B] - - [#xA723] - - [#xA725] - - [#xA727] - - [#xA729] - - [#xA72B] - - [#xA72D] - - [#xA72F-#xA731] - - [#xA733] - - [#xA735] - - [#xA737] - - [#xA739] - - [#xA73B] - - [#xA73D] - - [#xA73F] - - [#xA741] - - [#xA743] - - [#xA745] - - [#xA747] - - [#xA749] - - [#xA74B] - - [#xA74D] - - [#xA74F] - - [#xA751] - - [#xA753] - - [#xA755] - - [#xA757] - - [#xA759] - - [#xA75B] - - [#xA75D] - - [#xA75F] - - [#xA761] - - [#xA763] - - [#xA765] - - [#xA767] - - [#xA769] - - [#xA76B] - - [#xA76D] - - [#xA76F] - - [#xA771-#xA778] - - [#xA77A] - - [#xA77C] - - [#xA77F] - - [#xA781] - - [#xA783] - - [#xA785] - - [#xA787] - - [#xA78C] - - [#xA78E] - - [#xA791] - - [#xA793-#xA795] - - [#xA797] - - [#xA799] - - [#xA79B] - - [#xA79D] - - [#xA79F] - - [#xA7A1] - - [#xA7A3] - - [#xA7A5] - - [#xA7A7] - - [#xA7A9] - - [#xA7AF] - - [#xA7B5] - - [#xA7B7] - - [#xA7B9] - - [#xA7BB] - - [#xA7BD] - - [#xA7BF] - - [#xA7C1] - - [#xA7C3] - - [#xA7C8] - - [#xA7CA] - - [#xA7D1] - - [#xA7D3] - - [#xA7D5] - - [#xA7D7] - - [#xA7D9] - - [#xA7F6] - - [#xA7FA] - - [#xAB30-#xAB5A] - - [#xAB60-#xAB68] - - [#xAB70-#xABBF] - - [#xFB00-#xFB06] - - [#xFB13-#xFB17] - - [#xFF41-#xFF5A] - - -
- -
Ll       ::= [a-z#xB5#xDF-#xF6#xF8-#xFF#x101#x103#x105#x107#x109#x10B#x10D#x10F#x111#x113#x115#x117#x119#x11B#x11D#x11F#x121#x123#x125#x127#x129#x12B#x12D#x12F#x131#x133#x135#x137-#x138#x13A#x13C#x13E#x140#x142#x144#x146#x148-#x149#x14B#x14D#x14F#x151#x153#x155#x157#x159#x15B#x15D#x15F#x161#x163#x165#x167#x169#x16B#x16D#x16F#x171#x173#x175#x177#x17A#x17C#x17E-#x180#x183#x185#x188#x18C-#x18D#x192#x195#x199-#x19B#x19E#x1A1#x1A3#x1A5#x1A8#x1AA-#x1AB#x1AD#x1B0#x1B4#x1B6#x1B9-#x1BA#x1BD-#x1BF#x1C6#x1C9#x1CC#x1CE#x1D0#x1D2#x1D4#x1D6#x1D8#x1DA#x1DC-#x1DD#x1DF#x1E1#x1E3#x1E5#x1E7#x1E9#x1EB#x1ED#x1EF-#x1F0#x1F3#x1F5#x1F9#x1FB#x1FD#x1FF#x201#x203#x205#x207#x209#x20B#x20D#x20F#x211#x213#x215#x217#x219#x21B#x21D#x21F#x221#x223#x225#x227#x229#x22B#x22D#x22F#x231#x233-#x239#x23C#x23F-#x240#x242#x247#x249#x24B#x24D#x24F-#x293#x295-#x2AF#x371#x373#x377#x37B-#x37D#x390#x3AC-#x3CE#x3D0-#x3D1#x3D5-#x3D7#x3D9#x3DB#x3DD#x3DF#x3E1#x3E3#x3E5#x3E7#x3E9#x3EB#x3ED#x3EF-#x3F3#x3F5#x3F8#x3FB-#x3FC#x430-#x45F#x461#x463#x465#x467#x469#x46B#x46D#x46F#x471#x473#x475#x477#x479#x47B#x47D#x47F#x481#x48B#x48D#x48F#x491#x493#x495#x497#x499#x49B#x49D#x49F#x4A1#x4A3#x4A5#x4A7#x4A9#x4AB#x4AD#x4AF#x4B1#x4B3#x4B5#x4B7#x4B9#x4BB#x4BD#x4BF#x4C2#x4C4#x4C6#x4C8#x4CA#x4CC#x4CE-#x4CF#x4D1#x4D3#x4D5#x4D7#x4D9#x4DB#x4DD#x4DF#x4E1#x4E3#x4E5#x4E7#x4E9#x4EB#x4ED#x4EF#x4F1#x4F3#x4F5#x4F7#x4F9#x4FB#x4FD#x4FF#x501#x503#x505#x507#x509#x50B#x50D#x50F#x511#x513#x515#x517#x519#x51B#x51D#x51F#x521#x523#x525#x527#x529#x52B#x52D#x52F#x560-#x588#x10D0-#x10FA#x10FD-#x10FF#x13F8-#x13FD#x1C80-#x1C88#x1D00-#x1D2B#x1D6B-#x1D77#x1D79-#x1D9A#x1E01#x1E03#x1E05#x1E07#x1E09#x1E0B#x1E0D#x1E0F#x1E11#x1E13#x1E15#x1E17#x1E19#x1E1B#x1E1D#x1E1F#x1E21#x1E23#x1E25#x1E27#x1E29#x1E2B#x1E2D#x1E2F#x1E31#x1E33#x1E35#x1E37#x1E39#x1E3B#x1E3D#x1E3F#x1E41#x1E43#x1E45#x1E47#x1E49#x1E4B#x1E4D#x1E4F#x1E51#x1E53#x1E55#x1E57#x1E59#x1E5B#x1E5D#x1E5F#x1E61#x1E63#x1E65#x1E67#x1E69#x1E6B#x1E6D#x1E6F#x1E71#x1E73#x1E75#x1E77#x1E79#x1E7B#x1E7D#x1E7F#x1E81#x1E83#x1E85#x1E87#x1E89#x1E8B#x1E8D#x1E8F#x1E91#x1E93#x1E95-#x1E9D#x1E9F#x1EA1#x1EA3#x1EA5#x1EA7#x1EA9#x1EAB#x1EAD#x1EAF#x1EB1#x1EB3#x1EB5#x1EB7#x1EB9#x1EBB#x1EBD#x1EBF#x1EC1#x1EC3#x1EC5#x1EC7#x1EC9#x1ECB#x1ECD#x1ECF#x1ED1#x1ED3#x1ED5#x1ED7#x1ED9#x1EDB#x1EDD#x1EDF#x1EE1#x1EE3#x1EE5#x1EE7#x1EE9#x1EEB#x1EED#x1EEF#x1EF1#x1EF3#x1EF5#x1EF7#x1EF9#x1EFB#x1EFD#x1EFF-#x1F07#x1F10-#x1F15#x1F20-#x1F27#x1F30-#x1F37#x1F40-#x1F45#x1F50-#x1F57#x1F60-#x1F67#x1F70-#x1F7D#x1F80-#x1F87#x1F90-#x1F97#x1FA0-#x1FA7#x1FB0-#x1FB4#x1FB6-#x1FB7#x1FBE#x1FC2-#x1FC4#x1FC6-#x1FC7#x1FD0-#x1FD3#x1FD6-#x1FD7#x1FE0-#x1FE7#x1FF2-#x1FF4#x1FF6-#x1FF7#x210A#x210E-#x210F#x2113#x212F#x2134#x2139#x213C-#x213D#x2146-#x2149#x214E#x2184#x2C30-#x2C5F#x2C61#x2C65-#x2C66#x2C68#x2C6A#x2C6C#x2C71#x2C73-#x2C74#x2C76-#x2C7B#x2C81#x2C83#x2C85#x2C87#x2C89#x2C8B#x2C8D#x2C8F#x2C91#x2C93#x2C95#x2C97#x2C99#x2C9B#x2C9D#x2C9F#x2CA1#x2CA3#x2CA5#x2CA7#x2CA9#x2CAB#x2CAD#x2CAF#x2CB1#x2CB3#x2CB5#x2CB7#x2CB9#x2CBB#x2CBD#x2CBF#x2CC1#x2CC3#x2CC5#x2CC7#x2CC9#x2CCB#x2CCD#x2CCF#x2CD1#x2CD3#x2CD5#x2CD7#x2CD9#x2CDB#x2CDD#x2CDF#x2CE1#x2CE3-#x2CE4#x2CEC#x2CEE#x2CF3#x2D00-#x2D25#x2D27#x2D2D#xA641#xA643#xA645#xA647#xA649#xA64B#xA64D#xA64F#xA651#xA653#xA655#xA657#xA659#xA65B#xA65D#xA65F#xA661#xA663#xA665#xA667#xA669#xA66B#xA66D#xA681#xA683#xA685#xA687#xA689#xA68B#xA68D#xA68F#xA691#xA693#xA695#xA697#xA699#xA69B#xA723#xA725#xA727#xA729#xA72B#xA72D#xA72F-#xA731#xA733#xA735#xA737#xA739#xA73B#xA73D#xA73F#xA741#xA743#xA745#xA747#xA749#xA74B#xA74D#xA74F#xA751#xA753#xA755#xA757#xA759#xA75B#xA75D#xA75F#xA761#xA763#xA765#xA767#xA769#xA76B#xA76D#xA76F#xA771-#xA778#xA77A#xA77C#xA77F#xA781#xA783#xA785#xA787#xA78C#xA78E#xA791#xA793-#xA795#xA797#xA799#xA79B#xA79D#xA79F#xA7A1#xA7A3#xA7A5#xA7A7#xA7A9#xA7AF#xA7B5#xA7B7#xA7B9#xA7BB#xA7BD#xA7BF#xA7C1#xA7C3#xA7C8#xA7CA#xA7D1#xA7D3#xA7D5#xA7D7#xA7D9#xA7F6#xA7FA#xAB30-#xAB5A#xAB60-#xAB68#xAB70-#xABBF#xFB00-#xFB06#xFB13-#xFB17#xFF41-#xFF5A]
-
- Referenced by: -
- - -====================================================================================================================== -Lm -====================================================================================================================== - - -.. raw:: html - - - - - - [#x2B0-#x2C1] - - [#x2C6-#x2D1] - - [#x2E0-#x2E4] - - [#x2EC] - - [#x2EE] - - [#x374] - - [#x37A] - - [#x559] - - [#x640] - - [#x6E5-#x6E6] - - [#x7F4-#x7F5] - - [#x7FA] - - [#x81A] - - [#x824] - - [#x828] - - [#x8C9] - - [#x971] - - [#xE46] - - [#xEC6] - - [#x10FC] - - [#x17D7] - - [#x1843] - - [#x1AA7] - - [#x1C78-#x1C7D] - - [#x1D2C-#x1D6A] - - [#x1D78] - - [#x1D9B-#x1DBF] - - [#x2071] - - [#x207F] - - [#x2090-#x209C] - - [#x2C7C-#x2C7D] - - [#x2D6F] - - [#x2E2F] - - [#x3005] - - [#x3031-#x3035] - - [#x303B] - - [#x309D-#x309E] - - [#x30FC-#x30FE] - - [#xA015] - - [#xA4F8-#xA4FD] - - [#xA60C] - - [#xA67F] - - [#xA69C-#xA69D] - - [#xA717-#xA71F] - - [#xA770] - - [#xA788] - - [#xA7F2-#xA7F4] - - [#xA7F8-#xA7F9] - - [#xA9CF] - - [#xA9E6] - - [#xAA70] - - [#xAADD] - - [#xAAF3-#xAAF4] - - [#xAB5C-#xAB5F] - - [#xAB69] - - [#xFF70] - - [#xFF9E-#xFF9F] - - -
- -
Lm       ::= [#x2B0-#x2C1#x2C6-#x2D1#x2E0-#x2E4#x2EC#x2EE#x374#x37A#x559#x640#x6E5-#x6E6#x7F4-#x7F5#x7FA#x81A#x824#x828#x8C9#x971#xE46#xEC6#x10FC#x17D7#x1843#x1AA7#x1C78-#x1C7D#x1D2C-#x1D6A#x1D78#x1D9B-#x1DBF#x2071#x207F#x2090-#x209C#x2C7C-#x2C7D#x2D6F#x2E2F#x3005#x3031-#x3035#x303B#x309D-#x309E#x30FC-#x30FE#xA015#xA4F8-#xA4FD#xA60C#xA67F#xA69C-#xA69D#xA717-#xA71F#xA770#xA788#xA7F2-#xA7F4#xA7F8-#xA7F9#xA9CF#xA9E6#xAA70#xAADD#xAAF3-#xAAF4#xAB5C-#xAB5F#xAB69#xFF70#xFF9E-#xFF9F]
-
- Referenced by: -
- - -====================================================================================================================== -Lo -====================================================================================================================== - - -.. raw:: html - - - - - - [#xAA] - - [#xBA] - - [#x1BB] - - [#x1C0-#x1C3] - - [#x294] - - [#x5D0-#x5EA] - - [#x5EF-#x5F2] - - [#x620-#x63F] - - [#x641-#x64A] - - [#x66E-#x66F] - - [#x671-#x6D3] - - [#x6D5] - - [#x6EE-#x6EF] - - [#x6FA-#x6FC] - - [#x6FF] - - [#x710] - - [#x712-#x72F] - - [#x74D-#x7A5] - - [#x7B1] - - [#x7CA-#x7EA] - - [#x800-#x815] - - [#x840-#x858] - - [#x860-#x86A] - - [#x870-#x887] - - [#x889-#x88E] - - [#x8A0-#x8C8] - - [#x904-#x939] - - [#x93D] - - [#x950] - - [#x958-#x961] - - [#x972-#x980] - - [#x985-#x98C] - - [#x98F-#x990] - - [#x993-#x9A8] - - [#x9AA-#x9B0] - - [#x9B2] - - [#x9B6-#x9B9] - - [#x9BD] - - [#x9CE] - - [#x9DC-#x9DD] - - [#x9DF-#x9E1] - - [#x9F0-#x9F1] - - [#x9FC] - - [#xA05-#xA0A] - - [#xA0F-#xA10] - - [#xA13-#xA28] - - [#xA2A-#xA30] - - [#xA32-#xA33] - - [#xA35-#xA36] - - [#xA38-#xA39] - - [#xA59-#xA5C] - - [#xA5E] - - [#xA72-#xA74] - - [#xA85-#xA8D] - - [#xA8F-#xA91] - - [#xA93-#xAA8] - - [#xAAA-#xAB0] - - [#xAB2-#xAB3] - - [#xAB5-#xAB9] - - [#xABD] - - [#xAD0] - - [#xAE0-#xAE1] - - [#xAF9] - - [#xB05-#xB0C] - - [#xB0F-#xB10] - - [#xB13-#xB28] - - [#xB2A-#xB30] - - [#xB32-#xB33] - - [#xB35-#xB39] - - [#xB3D] - - [#xB5C-#xB5D] - - [#xB5F-#xB61] - - [#xB71] - - [#xB83] - - [#xB85-#xB8A] - - [#xB8E-#xB90] - - [#xB92-#xB95] - - [#xB99-#xB9A] - - [#xB9C] - - [#xB9E-#xB9F] - - [#xBA3-#xBA4] - - [#xBA8-#xBAA] - - [#xBAE-#xBB9] - - [#xBD0] - - [#xC05-#xC0C] - - [#xC0E-#xC10] - - [#xC12-#xC28] - - [#xC2A-#xC39] - - [#xC3D] - - [#xC58-#xC5A] - - [#xC5D] - - [#xC60-#xC61] - - [#xC80] - - [#xC85-#xC8C] - - [#xC8E-#xC90] - - [#xC92-#xCA8] - - [#xCAA-#xCB3] - - [#xCB5-#xCB9] - - [#xCBD] - - [#xCDD-#xCDE] - - [#xCE0-#xCE1] - - [#xCF1-#xCF2] - - [#xD04-#xD0C] - - [#xD0E-#xD10] - - [#xD12-#xD3A] - - [#xD3D] - - [#xD4E] - - [#xD54-#xD56] - - [#xD5F-#xD61] - - [#xD7A-#xD7F] - - [#xD85-#xD96] - - [#xD9A-#xDB1] - - [#xDB3-#xDBB] - - [#xDBD] - - [#xDC0-#xDC6] - - [#xE01-#xE30] - - [#xE32-#xE33] - - [#xE40-#xE45] - - [#xE81-#xE82] - - [#xE84] - - [#xE86-#xE8A] - - [#xE8C-#xEA3] - - [#xEA5] - - [#xEA7-#xEB0] - - [#xEB2-#xEB3] - - [#xEBD] - - [#xEC0-#xEC4] - - [#xEDC-#xEDF] - - [#xF00] - - [#xF40-#xF47] - - [#xF49-#xF6C] - - [#xF88-#xF8C] - - [#x1000-#x102A] - - [#x103F] - - [#x1050-#x1055] - - [#x105A-#x105D] - - [#x1061] - - [#x1065-#x1066] - - [#x106E-#x1070] - - [#x1075-#x1081] - - [#x108E] - - [#x1100-#x1248] - - [#x124A-#x124D] - - [#x1250-#x1256] - - [#x1258] - - [#x125A-#x125D] - - [#x1260-#x1288] - - [#x128A-#x128D] - - [#x1290-#x12B0] - - [#x12B2-#x12B5] - - [#x12B8-#x12BE] - - [#x12C0] - - [#x12C2-#x12C5] - - [#x12C8-#x12D6] - - [#x12D8-#x1310] - - [#x1312-#x1315] - - [#x1318-#x135A] - - [#x1380-#x138F] - - [#x1401-#x166C] - - [#x166F-#x167F] - - [#x1681-#x169A] - - [#x16A0-#x16EA] - - [#x16F1-#x16F8] - - [#x1700-#x1711] - - [#x171F-#x1731] - - [#x1740-#x1751] - - [#x1760-#x176C] - - [#x176E-#x1770] - - [#x1780-#x17B3] - - [#x17DC] - - [#x1820-#x1842] - - [#x1844-#x1878] - - [#x1880-#x1884] - - [#x1887-#x18A8] - - [#x18AA] - - [#x18B0-#x18F5] - - [#x1900-#x191E] - - [#x1950-#x196D] - - [#x1970-#x1974] - - [#x1980-#x19AB] - - [#x19B0-#x19C9] - - [#x1A00-#x1A16] - - [#x1A20-#x1A54] - - [#x1B05-#x1B33] - - [#x1B45-#x1B4C] - - [#x1B83-#x1BA0] - - [#x1BAE-#x1BAF] - - [#x1BBA-#x1BE5] - - [#x1C00-#x1C23] - - [#x1C4D-#x1C4F] - - [#x1C5A-#x1C77] - - [#x1CE9-#x1CEC] - - [#x1CEE-#x1CF3] - - [#x1CF5-#x1CF6] - - [#x1CFA] - - [#x2135-#x2138] - - [#x2D30-#x2D67] - - [#x2D80-#x2D96] - - [#x2DA0-#x2DA6] - - [#x2DA8-#x2DAE] - - [#x2DB0-#x2DB6] - - [#x2DB8-#x2DBE] - - [#x2DC0-#x2DC6] - - [#x2DC8-#x2DCE] - - [#x2DD0-#x2DD6] - - [#x2DD8-#x2DDE] - - [#x3006] - - [#x303C] - - [#x3041-#x3096] - - [#x309F] - - [#x30A1-#x30FA] - - [#x30FF] - - [#x3105-#x312F] - - [#x3131-#x318E] - - [#x31A0-#x31BF] - - [#x31F0-#x31FF] - - [#x4DBF] - - [#x9FFF-#xA014] - - [#xA016-#xA48C] - - [#xA4D0-#xA4F7] - - [#xA500-#xA60B] - - [#xA610-#xA61F] - - [#xA62A-#xA62B] - - [#xA66E] - - [#xA6A0-#xA6E5] - - [#xA78F] - - [#xA7F7] - - [#xA7FB-#xA801] - - [#xA803-#xA805] - - [#xA807-#xA80A] - - [#xA80C-#xA822] - - [#xA840-#xA873] - - [#xA882-#xA8B3] - - [#xA8F2-#xA8F7] - - [#xA8FB] - - [#xA8FD-#xA8FE] - - [#xA90A-#xA925] - - [#xA930-#xA946] - - [#xA960-#xA97C] - - [#xA984-#xA9B2] - - [#xA9E0-#xA9E4] - - [#xA9E7-#xA9EF] - - [#xA9FA-#xA9FE] - - [#xAA00-#xAA28] - - [#xAA40-#xAA42] - - [#xAA44-#xAA4B] - - [#xAA60-#xAA6F] - - [#xAA71-#xAA76] - - [#xAA7A] - - [#xAA7E-#xAAAF] - - [#xAAB1] - - [#xAAB5-#xAAB6] - - [#xAAB9-#xAABD] - - [#xAAC0] - - [#xAAC2] - - [#xAADB-#xAADC] - - [#xAAE0-#xAAEA] - - [#xAAF2] - - [#xAB01-#xAB06] - - [#xAB09-#xAB0E] - - [#xAB11-#xAB16] - - [#xAB20-#xAB26] - - [#xAB28-#xAB2E] - - [#xABC0-#xABE2] - - [#xD7A3] - - [#xD7B0-#xD7C6] - - [#xD7CB-#xD7FB] - - [#xF900-#xFA6D] - - [#xFA70-#xFAD9] - - [#xFB1D] - - [#xFB1F-#xFB28] - - [#xFB2A-#xFB36] - - [#xFB38-#xFB3C] - - [#xFB3E] - - [#xFB40-#xFB41] - - [#xFB43-#xFB44] - - [#xFB46-#xFBB1] - - [#xFBD3-#xFD3D] - - [#xFD50-#xFD8F] - - [#xFD92-#xFDC7] - - [#xFDF0-#xFDFB] - - [#xFE70-#xFE74] - - [#xFE76-#xFEFC] - - [#xFF66-#xFF6F] - - [#xFF71-#xFF9D] - - [#xFFA0-#xFFBE] - - [#xFFC2-#xFFC7] - - [#xFFCA-#xFFCF] - - [#xFFD2-#xFFD7] - - [#xFFDA-#xFFDC] - - -
- -
Lo       ::= [#xAA#xBA#x1BB#x1C0-#x1C3#x294#x5D0-#x5EA#x5EF-#x5F2#x620-#x63F#x641-#x64A#x66E-#x66F#x671-#x6D3#x6D5#x6EE-#x6EF#x6FA-#x6FC#x6FF#x710#x712-#x72F#x74D-#x7A5#x7B1#x7CA-#x7EA#x800-#x815#x840-#x858#x860-#x86A#x870-#x887#x889-#x88E#x8A0-#x8C8#x904-#x939#x93D#x950#x958-#x961#x972-#x980#x985-#x98C#x98F-#x990#x993-#x9A8#x9AA-#x9B0#x9B2#x9B6-#x9B9#x9BD#x9CE#x9DC-#x9DD#x9DF-#x9E1#x9F0-#x9F1#x9FC#xA05-#xA0A#xA0F-#xA10#xA13-#xA28#xA2A-#xA30#xA32-#xA33#xA35-#xA36#xA38-#xA39#xA59-#xA5C#xA5E#xA72-#xA74#xA85-#xA8D#xA8F-#xA91#xA93-#xAA8#xAAA-#xAB0#xAB2-#xAB3#xAB5-#xAB9#xABD#xAD0#xAE0-#xAE1#xAF9#xB05-#xB0C#xB0F-#xB10#xB13-#xB28#xB2A-#xB30#xB32-#xB33#xB35-#xB39#xB3D#xB5C-#xB5D#xB5F-#xB61#xB71#xB83#xB85-#xB8A#xB8E-#xB90#xB92-#xB95#xB99-#xB9A#xB9C#xB9E-#xB9F#xBA3-#xBA4#xBA8-#xBAA#xBAE-#xBB9#xBD0#xC05-#xC0C#xC0E-#xC10#xC12-#xC28#xC2A-#xC39#xC3D#xC58-#xC5A#xC5D#xC60-#xC61#xC80#xC85-#xC8C#xC8E-#xC90#xC92-#xCA8#xCAA-#xCB3#xCB5-#xCB9#xCBD#xCDD-#xCDE#xCE0-#xCE1#xCF1-#xCF2#xD04-#xD0C#xD0E-#xD10#xD12-#xD3A#xD3D#xD4E#xD54-#xD56#xD5F-#xD61#xD7A-#xD7F#xD85-#xD96#xD9A-#xDB1#xDB3-#xDBB#xDBD#xDC0-#xDC6#xE01-#xE30#xE32-#xE33#xE40-#xE45#xE81-#xE82#xE84#xE86-#xE8A#xE8C-#xEA3#xEA5#xEA7-#xEB0#xEB2-#xEB3#xEBD#xEC0-#xEC4#xEDC-#xEDF#xF00#xF40-#xF47#xF49-#xF6C#xF88-#xF8C#x1000-#x102A#x103F#x1050-#x1055#x105A-#x105D#x1061#x1065-#x1066#x106E-#x1070#x1075-#x1081#x108E#x1100-#x1248#x124A-#x124D#x1250-#x1256#x1258#x125A-#x125D#x1260-#x1288#x128A-#x128D#x1290-#x12B0#x12B2-#x12B5#x12B8-#x12BE#x12C0#x12C2-#x12C5#x12C8-#x12D6#x12D8-#x1310#x1312-#x1315#x1318-#x135A#x1380-#x138F#x1401-#x166C#x166F-#x167F#x1681-#x169A#x16A0-#x16EA#x16F1-#x16F8#x1700-#x1711#x171F-#x1731#x1740-#x1751#x1760-#x176C#x176E-#x1770#x1780-#x17B3#x17DC#x1820-#x1842#x1844-#x1878#x1880-#x1884#x1887-#x18A8#x18AA#x18B0-#x18F5#x1900-#x191E#x1950-#x196D#x1970-#x1974#x1980-#x19AB#x19B0-#x19C9#x1A00-#x1A16#x1A20-#x1A54#x1B05-#x1B33#x1B45-#x1B4C#x1B83-#x1BA0#x1BAE-#x1BAF#x1BBA-#x1BE5#x1C00-#x1C23#x1C4D-#x1C4F#x1C5A-#x1C77#x1CE9-#x1CEC#x1CEE-#x1CF3#x1CF5-#x1CF6#x1CFA#x2135-#x2138#x2D30-#x2D67#x2D80-#x2D96#x2DA0-#x2DA6#x2DA8-#x2DAE#x2DB0-#x2DB6#x2DB8-#x2DBE#x2DC0-#x2DC6#x2DC8-#x2DCE#x2DD0-#x2DD6#x2DD8-#x2DDE#x3006#x303C#x3041-#x3096#x309F#x30A1-#x30FA#x30FF#x3105-#x312F#x3131-#x318E#x31A0-#x31BF#x31F0-#x31FF#x4DBF#x9FFF-#xA014#xA016-#xA48C#xA4D0-#xA4F7#xA500-#xA60B#xA610-#xA61F#xA62A-#xA62B#xA66E#xA6A0-#xA6E5#xA78F#xA7F7#xA7FB-#xA801#xA803-#xA805#xA807-#xA80A#xA80C-#xA822#xA840-#xA873#xA882-#xA8B3#xA8F2-#xA8F7#xA8FB#xA8FD-#xA8FE#xA90A-#xA925#xA930-#xA946#xA960-#xA97C#xA984-#xA9B2#xA9E0-#xA9E4#xA9E7-#xA9EF#xA9FA-#xA9FE#xAA00-#xAA28#xAA40-#xAA42#xAA44-#xAA4B#xAA60-#xAA6F#xAA71-#xAA76#xAA7A#xAA7E-#xAAAF#xAAB1#xAAB5-#xAAB6#xAAB9-#xAABD#xAAC0#xAAC2#xAADB-#xAADC#xAAE0-#xAAEA#xAAF2#xAB01-#xAB06#xAB09-#xAB0E#xAB11-#xAB16#xAB20-#xAB26#xAB28-#xAB2E#xABC0-#xABE2#xD7A3#xD7B0-#xD7C6#xD7CB-#xD7FB#xF900-#xFA6D#xFA70-#xFAD9#xFB1D#xFB1F-#xFB28#xFB2A-#xFB36#xFB38-#xFB3C#xFB3E#xFB40-#xFB41#xFB43-#xFB44#xFB46-#xFBB1#xFBD3-#xFD3D#xFD50-#xFD8F#xFD92-#xFDC7#xFDF0-#xFDFB#xFE70-#xFE74#xFE76-#xFEFC#xFF66-#xFF6F#xFF71-#xFF9D#xFFA0-#xFFBE#xFFC2-#xFFC7#xFFCA-#xFFCF#xFFD2-#xFFD7#xFFDA-#xFFDC]
-
- Referenced by: -
- - -====================================================================================================================== -Lt -====================================================================================================================== - - -.. raw:: html - - - - - - [#x1C5] - - [#x1C8] - - [#x1CB] - - [#x1F2] - - [#x1F88-#x1F8F] - - [#x1F98-#x1F9F] - - [#x1FA8-#x1FAF] - - [#x1FBC] - - [#x1FCC] - - [#x1FFC] - - -
- -
Lt       ::= [#x1C5#x1C8#x1CB#x1F2#x1F88-#x1F8F#x1F98-#x1F9F#x1FA8-#x1FAF#x1FBC#x1FCC#x1FFC]
-
- Referenced by: -
- - -====================================================================================================================== -Lu -====================================================================================================================== - - -.. raw:: html - - - - - - [A-Z] - - [#xC0-#xD6] - - [#xD8-#xDE] - - [#x100] - - [#x102] - - [#x104] - - [#x106] - - [#x108] - - [#x10A] - - [#x10C] - - [#x10E] - - [#x110] - - [#x112] - - [#x114] - - [#x116] - - [#x118] - - [#x11A] - - [#x11C] - - [#x11E] - - [#x120] - - [#x122] - - [#x124] - - [#x126] - - [#x128] - - [#x12A] - - [#x12C] - - [#x12E] - - [#x130] - - [#x132] - - [#x134] - - [#x136] - - [#x139] - - [#x13B] - - [#x13D] - - [#x13F] - - [#x141] - - [#x143] - - [#x145] - - [#x147] - - [#x14A] - - [#x14C] - - [#x14E] - - [#x150] - - [#x152] - - [#x154] - - [#x156] - - [#x158] - - [#x15A] - - [#x15C] - - [#x15E] - - [#x160] - - [#x162] - - [#x164] - - [#x166] - - [#x168] - - [#x16A] - - [#x16C] - - [#x16E] - - [#x170] - - [#x172] - - [#x174] - - [#x176] - - [#x178-#x179] - - [#x17B] - - [#x17D] - - [#x181-#x182] - - [#x184] - - [#x186-#x187] - - [#x189-#x18B] - - [#x18E-#x191] - - [#x193-#x194] - - [#x196-#x198] - - [#x19C-#x19D] - - [#x19F-#x1A0] - - [#x1A2] - - [#x1A4] - - [#x1A6-#x1A7] - - [#x1A9] - - [#x1AC] - - [#x1AE-#x1AF] - - [#x1B1-#x1B3] - - [#x1B5] - - [#x1B7-#x1B8] - - [#x1BC] - - [#x1C4] - - [#x1C7] - - [#x1CA] - - [#x1CD] - - [#x1CF] - - [#x1D1] - - [#x1D3] - - [#x1D5] - - [#x1D7] - - [#x1D9] - - [#x1DB] - - [#x1DE] - - [#x1E0] - - [#x1E2] - - [#x1E4] - - [#x1E6] - - [#x1E8] - - [#x1EA] - - [#x1EC] - - [#x1EE] - - [#x1F1] - - [#x1F4] - - [#x1F6-#x1F8] - - [#x1FA] - - [#x1FC] - - [#x1FE] - - [#x200] - - [#x202] - - [#x204] - - [#x206] - - [#x208] - - [#x20A] - - [#x20C] - - [#x20E] - - [#x210] - - [#x212] - - [#x214] - - [#x216] - - [#x218] - - [#x21A] - - [#x21C] - - [#x21E] - - [#x220] - - [#x222] - - [#x224] - - [#x226] - - [#x228] - - [#x22A] - - [#x22C] - - [#x22E] - - [#x230] - - [#x232] - - [#x23A-#x23B] - - [#x23D-#x23E] - - [#x241] - - [#x243-#x246] - - [#x248] - - [#x24A] - - [#x24C] - - [#x24E] - - [#x370] - - [#x372] - - [#x376] - - [#x37F] - - [#x386] - - [#x388-#x38A] - - [#x38C] - - [#x38E-#x38F] - - [#x391-#x3A1] - - [#x3A3-#x3AB] - - [#x3CF] - - [#x3D2-#x3D4] - - [#x3D8] - - [#x3DA] - - [#x3DC] - - [#x3DE] - - [#x3E0] - - [#x3E2] - - [#x3E4] - - [#x3E6] - - [#x3E8] - - [#x3EA] - - [#x3EC] - - [#x3EE] - - [#x3F4] - - [#x3F7] - - [#x3F9-#x3FA] - - [#x3FD-#x42F] - - [#x460] - - [#x462] - - [#x464] - - [#x466] - - [#x468] - - [#x46A] - - [#x46C] - - [#x46E] - - [#x470] - - [#x472] - - [#x474] - - [#x476] - - [#x478] - - [#x47A] - - [#x47C] - - [#x47E] - - [#x480] - - [#x48A] - - [#x48C] - - [#x48E] - - [#x490] - - [#x492] - - [#x494] - - [#x496] - - [#x498] - - [#x49A] - - [#x49C] - - [#x49E] - - [#x4A0] - - [#x4A2] - - [#x4A4] - - [#x4A6] - - [#x4A8] - - [#x4AA] - - [#x4AC] - - [#x4AE] - - [#x4B0] - - [#x4B2] - - [#x4B4] - - [#x4B6] - - [#x4B8] - - [#x4BA] - - [#x4BC] - - [#x4BE] - - [#x4C0-#x4C1] - - [#x4C3] - - [#x4C5] - - [#x4C7] - - [#x4C9] - - [#x4CB] - - [#x4CD] - - [#x4D0] - - [#x4D2] - - [#x4D4] - - [#x4D6] - - [#x4D8] - - [#x4DA] - - [#x4DC] - - [#x4DE] - - [#x4E0] - - [#x4E2] - - [#x4E4] - - [#x4E6] - - [#x4E8] - - [#x4EA] - - [#x4EC] - - [#x4EE] - - [#x4F0] - - [#x4F2] - - [#x4F4] - - [#x4F6] - - [#x4F8] - - [#x4FA] - - [#x4FC] - - [#x4FE] - - [#x500] - - [#x502] - - [#x504] - - [#x506] - - [#x508] - - [#x50A] - - [#x50C] - - [#x50E] - - [#x510] - - [#x512] - - [#x514] - - [#x516] - - [#x518] - - [#x51A] - - [#x51C] - - [#x51E] - - [#x520] - - [#x522] - - [#x524] - - [#x526] - - [#x528] - - [#x52A] - - [#x52C] - - [#x52E] - - [#x531-#x556] - - [#x10A0-#x10C5] - - [#x10C7] - - [#x10CD] - - [#x13A0-#x13F5] - - [#x1C90-#x1CBA] - - [#x1CBD-#x1CBF] - - [#x1E00] - - [#x1E02] - - [#x1E04] - - [#x1E06] - - [#x1E08] - - [#x1E0A] - - [#x1E0C] - - [#x1E0E] - - [#x1E10] - - [#x1E12] - - [#x1E14] - - [#x1E16] - - [#x1E18] - - [#x1E1A] - - [#x1E1C] - - [#x1E1E] - - [#x1E20] - - [#x1E22] - - [#x1E24] - - [#x1E26] - - [#x1E28] - - [#x1E2A] - - [#x1E2C] - - [#x1E2E] - - [#x1E30] - - [#x1E32] - - [#x1E34] - - [#x1E36] - - [#x1E38] - - [#x1E3A] - - [#x1E3C] - - [#x1E3E] - - [#x1E40] - - [#x1E42] - - [#x1E44] - - [#x1E46] - - [#x1E48] - - [#x1E4A] - - [#x1E4C] - - [#x1E4E] - - [#x1E50] - - [#x1E52] - - [#x1E54] - - [#x1E56] - - [#x1E58] - - [#x1E5A] - - [#x1E5C] - - [#x1E5E] - - [#x1E60] - - [#x1E62] - - [#x1E64] - - [#x1E66] - - [#x1E68] - - [#x1E6A] - - [#x1E6C] - - [#x1E6E] - - [#x1E70] - - [#x1E72] - - [#x1E74] - - [#x1E76] - - [#x1E78] - - [#x1E7A] - - [#x1E7C] - - [#x1E7E] - - [#x1E80] - - [#x1E82] - - [#x1E84] - - [#x1E86] - - [#x1E88] - - [#x1E8A] - - [#x1E8C] - - [#x1E8E] - - [#x1E90] - - [#x1E92] - - [#x1E94] - - [#x1E9E] - - [#x1EA0] - - [#x1EA2] - - [#x1EA4] - - [#x1EA6] - - [#x1EA8] - - [#x1EAA] - - [#x1EAC] - - [#x1EAE] - - [#x1EB0] - - [#x1EB2] - - [#x1EB4] - - [#x1EB6] - - [#x1EB8] - - [#x1EBA] - - [#x1EBC] - - [#x1EBE] - - [#x1EC0] - - [#x1EC2] - - [#x1EC4] - - [#x1EC6] - - [#x1EC8] - - [#x1ECA] - - [#x1ECC] - - [#x1ECE] - - [#x1ED0] - - [#x1ED2] - - [#x1ED4] - - [#x1ED6] - - [#x1ED8] - - [#x1EDA] - - [#x1EDC] - - [#x1EDE] - - [#x1EE0] - - [#x1EE2] - - [#x1EE4] - - [#x1EE6] - - [#x1EE8] - - [#x1EEA] - - [#x1EEC] - - [#x1EEE] - - [#x1EF0] - - [#x1EF2] - - [#x1EF4] - - [#x1EF6] - - [#x1EF8] - - [#x1EFA] - - [#x1EFC] - - [#x1EFE] - - [#x1F08-#x1F0F] - - [#x1F18-#x1F1D] - - [#x1F28-#x1F2F] - - [#x1F38-#x1F3F] - - [#x1F48-#x1F4D] - - [#x1F59] - - [#x1F5B] - - [#x1F5D] - - [#x1F5F] - - [#x1F68-#x1F6F] - - [#x1FB8-#x1FBB] - - [#x1FC8-#x1FCB] - - [#x1FD8-#x1FDB] - - [#x1FE8-#x1FEC] - - [#x1FF8-#x1FFB] - - [#x2102] - - [#x2107] - - [#x210B-#x210D] - - [#x2110-#x2112] - - [#x2115] - - [#x2119-#x211D] - - [#x2124] - - [#x2126] - - [#x2128] - - [#x212A-#x212D] - - [#x2130-#x2133] - - [#x213E-#x213F] - - [#x2145] - - [#x2183] - - [#x2C00-#x2C2F] - - [#x2C60] - - [#x2C62-#x2C64] - - [#x2C67] - - [#x2C69] - - [#x2C6B] - - [#x2C6D-#x2C70] - - [#x2C72] - - [#x2C75] - - [#x2C7E-#x2C80] - - [#x2C82] - - [#x2C84] - - [#x2C86] - - [#x2C88] - - [#x2C8A] - - [#x2C8C] - - [#x2C8E] - - [#x2C90] - - [#x2C92] - - [#x2C94] - - [#x2C96] - - [#x2C98] - - [#x2C9A] - - [#x2C9C] - - [#x2C9E] - - [#x2CA0] - - [#x2CA2] - - [#x2CA4] - - [#x2CA6] - - [#x2CA8] - - [#x2CAA] - - [#x2CAC] - - [#x2CAE] - - [#x2CB0] - - [#x2CB2] - - [#x2CB4] - - [#x2CB6] - - [#x2CB8] - - [#x2CBA] - - [#x2CBC] - - [#x2CBE] - - [#x2CC0] - - [#x2CC2] - - [#x2CC4] - - [#x2CC6] - - [#x2CC8] - - [#x2CCA] - - [#x2CCC] - - [#x2CCE] - - [#x2CD0] - - [#x2CD2] - - [#x2CD4] - - [#x2CD6] - - [#x2CD8] - - [#x2CDA] - - [#x2CDC] - - [#x2CDE] - - [#x2CE0] - - [#x2CE2] - - [#x2CEB] - - [#x2CED] - - [#x2CF2] - - [#xA640] - - [#xA642] - - [#xA644] - - [#xA646] - - [#xA648] - - [#xA64A] - - [#xA64C] - - [#xA64E] - - [#xA650] - - [#xA652] - - [#xA654] - - [#xA656] - - [#xA658] - - [#xA65A] - - [#xA65C] - - [#xA65E] - - [#xA660] - - [#xA662] - - [#xA664] - - [#xA666] - - [#xA668] - - [#xA66A] - - [#xA66C] - - [#xA680] - - [#xA682] - - [#xA684] - - [#xA686] - - [#xA688] - - [#xA68A] - - [#xA68C] - - [#xA68E] - - [#xA690] - - [#xA692] - - [#xA694] - - [#xA696] - - [#xA698] - - [#xA69A] - - [#xA722] - - [#xA724] - - [#xA726] - - [#xA728] - - [#xA72A] - - [#xA72C] - - [#xA72E] - - [#xA732] - - [#xA734] - - [#xA736] - - [#xA738] - - [#xA73A] - - [#xA73C] - - [#xA73E] - - [#xA740] - - [#xA742] - - [#xA744] - - [#xA746] - - [#xA748] - - [#xA74A] - - [#xA74C] - - [#xA74E] - - [#xA750] - - [#xA752] - - [#xA754] - - [#xA756] - - [#xA758] - - [#xA75A] - - [#xA75C] - - [#xA75E] - - [#xA760] - - [#xA762] - - [#xA764] - - [#xA766] - - [#xA768] - - [#xA76A] - - [#xA76C] - - [#xA76E] - - [#xA779] - - [#xA77B] - - [#xA77D-#xA77E] - - [#xA780] - - [#xA782] - - [#xA784] - - [#xA786] - - [#xA78B] - - [#xA78D] - - [#xA790] - - [#xA792] - - [#xA796] - - [#xA798] - - [#xA79A] - - [#xA79C] - - [#xA79E] - - [#xA7A0] - - [#xA7A2] - - [#xA7A4] - - [#xA7A6] - - [#xA7A8] - - [#xA7AA-#xA7AE] - - [#xA7B0-#xA7B4] - - [#xA7B6] - - [#xA7B8] - - [#xA7BA] - - [#xA7BC] - - [#xA7BE] - - [#xA7C0] - - [#xA7C2] - - [#xA7C4-#xA7C7] - - [#xA7C9] - - [#xA7D0] - - [#xA7D6] - - [#xA7D8] - - [#xA7F5] - - [#xFF21-#xFF3A] - - -
- -
Lu       ::= [A-Z#xC0-#xD6#xD8-#xDE#x100#x102#x104#x106#x108#x10A#x10C#x10E#x110#x112#x114#x116#x118#x11A#x11C#x11E#x120#x122#x124#x126#x128#x12A#x12C#x12E#x130#x132#x134#x136#x139#x13B#x13D#x13F#x141#x143#x145#x147#x14A#x14C#x14E#x150#x152#x154#x156#x158#x15A#x15C#x15E#x160#x162#x164#x166#x168#x16A#x16C#x16E#x170#x172#x174#x176#x178-#x179#x17B#x17D#x181-#x182#x184#x186-#x187#x189-#x18B#x18E-#x191#x193-#x194#x196-#x198#x19C-#x19D#x19F-#x1A0#x1A2#x1A4#x1A6-#x1A7#x1A9#x1AC#x1AE-#x1AF#x1B1-#x1B3#x1B5#x1B7-#x1B8#x1BC#x1C4#x1C7#x1CA#x1CD#x1CF#x1D1#x1D3#x1D5#x1D7#x1D9#x1DB#x1DE#x1E0#x1E2#x1E4#x1E6#x1E8#x1EA#x1EC#x1EE#x1F1#x1F4#x1F6-#x1F8#x1FA#x1FC#x1FE#x200#x202#x204#x206#x208#x20A#x20C#x20E#x210#x212#x214#x216#x218#x21A#x21C#x21E#x220#x222#x224#x226#x228#x22A#x22C#x22E#x230#x232#x23A-#x23B#x23D-#x23E#x241#x243-#x246#x248#x24A#x24C#x24E#x370#x372#x376#x37F#x386#x388-#x38A#x38C#x38E-#x38F#x391-#x3A1#x3A3-#x3AB#x3CF#x3D2-#x3D4#x3D8#x3DA#x3DC#x3DE#x3E0#x3E2#x3E4#x3E6#x3E8#x3EA#x3EC#x3EE#x3F4#x3F7#x3F9-#x3FA#x3FD-#x42F#x460#x462#x464#x466#x468#x46A#x46C#x46E#x470#x472#x474#x476#x478#x47A#x47C#x47E#x480#x48A#x48C#x48E#x490#x492#x494#x496#x498#x49A#x49C#x49E#x4A0#x4A2#x4A4#x4A6#x4A8#x4AA#x4AC#x4AE#x4B0#x4B2#x4B4#x4B6#x4B8#x4BA#x4BC#x4BE#x4C0-#x4C1#x4C3#x4C5#x4C7#x4C9#x4CB#x4CD#x4D0#x4D2#x4D4#x4D6#x4D8#x4DA#x4DC#x4DE#x4E0#x4E2#x4E4#x4E6#x4E8#x4EA#x4EC#x4EE#x4F0#x4F2#x4F4#x4F6#x4F8#x4FA#x4FC#x4FE#x500#x502#x504#x506#x508#x50A#x50C#x50E#x510#x512#x514#x516#x518#x51A#x51C#x51E#x520#x522#x524#x526#x528#x52A#x52C#x52E#x531-#x556#x10A0-#x10C5#x10C7#x10CD#x13A0-#x13F5#x1C90-#x1CBA#x1CBD-#x1CBF#x1E00#x1E02#x1E04#x1E06#x1E08#x1E0A#x1E0C#x1E0E#x1E10#x1E12#x1E14#x1E16#x1E18#x1E1A#x1E1C#x1E1E#x1E20#x1E22#x1E24#x1E26#x1E28#x1E2A#x1E2C#x1E2E#x1E30#x1E32#x1E34#x1E36#x1E38#x1E3A#x1E3C#x1E3E#x1E40#x1E42#x1E44#x1E46#x1E48#x1E4A#x1E4C#x1E4E#x1E50#x1E52#x1E54#x1E56#x1E58#x1E5A#x1E5C#x1E5E#x1E60#x1E62#x1E64#x1E66#x1E68#x1E6A#x1E6C#x1E6E#x1E70#x1E72#x1E74#x1E76#x1E78#x1E7A#x1E7C#x1E7E#x1E80#x1E82#x1E84#x1E86#x1E88#x1E8A#x1E8C#x1E8E#x1E90#x1E92#x1E94#x1E9E#x1EA0#x1EA2#x1EA4#x1EA6#x1EA8#x1EAA#x1EAC#x1EAE#x1EB0#x1EB2#x1EB4#x1EB6#x1EB8#x1EBA#x1EBC#x1EBE#x1EC0#x1EC2#x1EC4#x1EC6#x1EC8#x1ECA#x1ECC#x1ECE#x1ED0#x1ED2#x1ED4#x1ED6#x1ED8#x1EDA#x1EDC#x1EDE#x1EE0#x1EE2#x1EE4#x1EE6#x1EE8#x1EEA#x1EEC#x1EEE#x1EF0#x1EF2#x1EF4#x1EF6#x1EF8#x1EFA#x1EFC#x1EFE#x1F08-#x1F0F#x1F18-#x1F1D#x1F28-#x1F2F#x1F38-#x1F3F#x1F48-#x1F4D#x1F59#x1F5B#x1F5D#x1F5F#x1F68-#x1F6F#x1FB8-#x1FBB#x1FC8-#x1FCB#x1FD8-#x1FDB#x1FE8-#x1FEC#x1FF8-#x1FFB#x2102#x2107#x210B-#x210D#x2110-#x2112#x2115#x2119-#x211D#x2124#x2126#x2128#x212A-#x212D#x2130-#x2133#x213E-#x213F#x2145#x2183#x2C00-#x2C2F#x2C60#x2C62-#x2C64#x2C67#x2C69#x2C6B#x2C6D-#x2C70#x2C72#x2C75#x2C7E-#x2C80#x2C82#x2C84#x2C86#x2C88#x2C8A#x2C8C#x2C8E#x2C90#x2C92#x2C94#x2C96#x2C98#x2C9A#x2C9C#x2C9E#x2CA0#x2CA2#x2CA4#x2CA6#x2CA8#x2CAA#x2CAC#x2CAE#x2CB0#x2CB2#x2CB4#x2CB6#x2CB8#x2CBA#x2CBC#x2CBE#x2CC0#x2CC2#x2CC4#x2CC6#x2CC8#x2CCA#x2CCC#x2CCE#x2CD0#x2CD2#x2CD4#x2CD6#x2CD8#x2CDA#x2CDC#x2CDE#x2CE0#x2CE2#x2CEB#x2CED#x2CF2#xA640#xA642#xA644#xA646#xA648#xA64A#xA64C#xA64E#xA650#xA652#xA654#xA656#xA658#xA65A#xA65C#xA65E#xA660#xA662#xA664#xA666#xA668#xA66A#xA66C#xA680#xA682#xA684#xA686#xA688#xA68A#xA68C#xA68E#xA690#xA692#xA694#xA696#xA698#xA69A#xA722#xA724#xA726#xA728#xA72A#xA72C#xA72E#xA732#xA734#xA736#xA738#xA73A#xA73C#xA73E#xA740#xA742#xA744#xA746#xA748#xA74A#xA74C#xA74E#xA750#xA752#xA754#xA756#xA758#xA75A#xA75C#xA75E#xA760#xA762#xA764#xA766#xA768#xA76A#xA76C#xA76E#xA779#xA77B#xA77D-#xA77E#xA780#xA782#xA784#xA786#xA78B#xA78D#xA790#xA792#xA796#xA798#xA79A#xA79C#xA79E#xA7A0#xA7A2#xA7A4#xA7A6#xA7A8#xA7AA-#xA7AE#xA7B0-#xA7B4#xA7B6#xA7B8#xA7BA#xA7BC#xA7BE#xA7C0#xA7C2#xA7C4-#xA7C7#xA7C9#xA7D0#xA7D6#xA7D8#xA7F5#xFF21-#xFF3A]
-
- Referenced by: -
- - -====================================================================================================================== -Nl -====================================================================================================================== - - -.. raw:: html - - - - - - [#x16EE-#x16F0] - - [#x2160-#x2182] - - [#x2185-#x2188] - - [#x3007] - - [#x3021-#x3029] - - [#x3038-#x303A] - - [#xA6E6-#xA6EF] - - -
- -
Nl       ::= [#x16EE-#x16F0#x2160-#x2182#x2185-#x2188#x3007#x3021-#x3029#x3038-#x303A#xA6E6-#xA6EF]
-
- Referenced by: -
- - -====================================================================================================================== -UnicodeIdentifierExtend -====================================================================================================================== - - -.. raw:: html - - - - - - Mn - - Mc - - Nd - - Pc - - Cf - - CJK - -
- - -
         ::= Mn
-
           | Mc
-
           | Nd
-
           | Pc
-
           | Cf
-
           | CJK
-
- - -====================================================================================================================== -Cf -====================================================================================================================== - - -.. raw:: html - - - - - - [#xAD] - - [#x600-#x605] - - [#x61C] - - [#x6DD] - - [#x70F] - - [#x890-#x891] - - [#x8E2] - - [#x180E] - - [#x200B-#x200F] - - [#x202A-#x202E] - - [#x2060-#x2064] - - [#x2066-#x206F] - - [#xFEFF] - - [#xFFF9-#xFFFB] - - -
- -
Cf       ::= [#xAD#x600-#x605#x61C#x6DD#x70F#x890-#x891#x8E2#x180E#x200B-#x200F#x202A-#x202E#x2060-#x2064#x2066-#x206F#xFEFF#xFFF9-#xFFFB]
-
- Referenced by: -
- - -====================================================================================================================== -Mc -====================================================================================================================== - - -.. raw:: html - - - - - - [#x903] - - [#x93B] - - [#x93E-#x940] - - [#x949-#x94C] - - [#x94E-#x94F] - - [#x982-#x983] - - [#x9BE-#x9C0] - - [#x9C7-#x9C8] - - [#x9CB-#x9CC] - - [#x9D7] - - [#xA03] - - [#xA3E-#xA40] - - [#xA83] - - [#xABE-#xAC0] - - [#xAC9] - - [#xACB-#xACC] - - [#xB02-#xB03] - - [#xB3E] - - [#xB40] - - [#xB47-#xB48] - - [#xB4B-#xB4C] - - [#xB57] - - [#xBBE-#xBBF] - - [#xBC1-#xBC2] - - [#xBC6-#xBC8] - - [#xBCA-#xBCC] - - [#xBD7] - - [#xC01-#xC03] - - [#xC41-#xC44] - - [#xC82-#xC83] - - [#xCBE] - - [#xCC0-#xCC4] - - [#xCC7-#xCC8] - - [#xCCA-#xCCB] - - [#xCD5-#xCD6] - - [#xCF3] - - [#xD02-#xD03] - - [#xD3E-#xD40] - - [#xD46-#xD48] - - [#xD4A-#xD4C] - - [#xD57] - - [#xD82-#xD83] - - [#xDCF-#xDD1] - - [#xDD8-#xDDF] - - [#xDF2-#xDF3] - - [#xF3E-#xF3F] - - [#xF7F] - - [#x102B-#x102C] - - [#x1031] - - [#x1038] - - [#x103B-#x103C] - - [#x1056-#x1057] - - [#x1062-#x1064] - - [#x1067-#x106D] - - [#x1083-#x1084] - - [#x1087-#x108C] - - [#x108F] - - [#x109A-#x109C] - - [#x1715] - - [#x1734] - - [#x17B6] - - [#x17BE-#x17C5] - - [#x17C7-#x17C8] - - [#x1923-#x1926] - - [#x1929-#x192B] - - [#x1930-#x1931] - - [#x1933-#x1938] - - [#x1A19-#x1A1A] - - [#x1A55] - - [#x1A57] - - [#x1A61] - - [#x1A63-#x1A64] - - [#x1A6D-#x1A72] - - [#x1B04] - - [#x1B35] - - [#x1B3B] - - [#x1B3D-#x1B41] - - [#x1B43-#x1B44] - - [#x1B82] - - [#x1BA1] - - [#x1BA6-#x1BA7] - - [#x1BAA] - - [#x1BE7] - - [#x1BEA-#x1BEC] - - [#x1BEE] - - [#x1BF2-#x1BF3] - - [#x1C24-#x1C2B] - - [#x1C34-#x1C35] - - [#x1CE1] - - [#x1CF7] - - [#x302E-#x302F] - - [#xA823-#xA824] - - [#xA827] - - [#xA880-#xA881] - - [#xA8B4-#xA8C3] - - [#xA952-#xA953] - - [#xA983] - - [#xA9B4-#xA9B5] - - [#xA9BA-#xA9BB] - - [#xA9BE-#xA9C0] - - [#xAA2F-#xAA30] - - [#xAA33-#xAA34] - - [#xAA4D] - - [#xAA7B] - - [#xAA7D] - - [#xAAEB] - - [#xAAEE-#xAAEF] - - [#xAAF5] - - [#xABE3-#xABE4] - - [#xABE6-#xABE7] - - [#xABE9-#xABEA] - - [#xABEC] - - -
- -
Mc       ::= [#x903#x93B#x93E-#x940#x949-#x94C#x94E-#x94F#x982-#x983#x9BE-#x9C0#x9C7-#x9C8#x9CB-#x9CC#x9D7#xA03#xA3E-#xA40#xA83#xABE-#xAC0#xAC9#xACB-#xACC#xB02-#xB03#xB3E#xB40#xB47-#xB48#xB4B-#xB4C#xB57#xBBE-#xBBF#xBC1-#xBC2#xBC6-#xBC8#xBCA-#xBCC#xBD7#xC01-#xC03#xC41-#xC44#xC82-#xC83#xCBE#xCC0-#xCC4#xCC7-#xCC8#xCCA-#xCCB#xCD5-#xCD6#xCF3#xD02-#xD03#xD3E-#xD40#xD46-#xD48#xD4A-#xD4C#xD57#xD82-#xD83#xDCF-#xDD1#xDD8-#xDDF#xDF2-#xDF3#xF3E-#xF3F#xF7F#x102B-#x102C#x1031#x1038#x103B-#x103C#x1056-#x1057#x1062-#x1064#x1067-#x106D#x1083-#x1084#x1087-#x108C#x108F#x109A-#x109C#x1715#x1734#x17B6#x17BE-#x17C5#x17C7-#x17C8#x1923-#x1926#x1929-#x192B#x1930-#x1931#x1933-#x1938#x1A19-#x1A1A#x1A55#x1A57#x1A61#x1A63-#x1A64#x1A6D-#x1A72#x1B04#x1B35#x1B3B#x1B3D-#x1B41#x1B43-#x1B44#x1B82#x1BA1#x1BA6-#x1BA7#x1BAA#x1BE7#x1BEA-#x1BEC#x1BEE#x1BF2-#x1BF3#x1C24-#x1C2B#x1C34-#x1C35#x1CE1#x1CF7#x302E-#x302F#xA823-#xA824#xA827#xA880-#xA881#xA8B4-#xA8C3#xA952-#xA953#xA983#xA9B4-#xA9B5#xA9BA-#xA9BB#xA9BE-#xA9C0#xAA2F-#xAA30#xAA33-#xAA34#xAA4D#xAA7B#xAA7D#xAAEB#xAAEE-#xAAEF#xAAF5#xABE3-#xABE4#xABE6-#xABE7#xABE9-#xABEA#xABEC]
-
- Referenced by: -
- - -====================================================================================================================== -Mn -====================================================================================================================== - - -.. raw:: html - - - - - - [#x300-#x36F] - - [#x483-#x487] - - [#x591-#x5BD] - - [#x5BF] - - [#x5C1-#x5C2] - - [#x5C4-#x5C5] - - [#x5C7] - - [#x610-#x61A] - - [#x64B-#x65F] - - [#x670] - - [#x6D6-#x6DC] - - [#x6DF-#x6E4] - - [#x6E7-#x6E8] - - [#x6EA-#x6ED] - - [#x711] - - [#x730-#x74A] - - [#x7A6-#x7B0] - - [#x7EB-#x7F3] - - [#x7FD] - - [#x816-#x819] - - [#x81B-#x823] - - [#x825-#x827] - - [#x829-#x82D] - - [#x859-#x85B] - - [#x898-#x89F] - - [#x8CA-#x8E1] - - [#x8E3-#x902] - - [#x93A] - - [#x93C] - - [#x941-#x948] - - [#x94D] - - [#x951-#x957] - - [#x962-#x963] - - [#x981] - - [#x9BC] - - [#x9C1-#x9C4] - - [#x9CD] - - [#x9E2-#x9E3] - - [#x9FE] - - [#xA01-#xA02] - - [#xA3C] - - [#xA41-#xA42] - - [#xA47-#xA48] - - [#xA4B-#xA4D] - - [#xA51] - - [#xA70-#xA71] - - [#xA75] - - [#xA81-#xA82] - - [#xABC] - - [#xAC1-#xAC5] - - [#xAC7-#xAC8] - - [#xACD] - - [#xAE2-#xAE3] - - [#xAFA-#xAFF] - - [#xB01] - - [#xB3C] - - [#xB3F] - - [#xB41-#xB44] - - [#xB4D] - - [#xB55-#xB56] - - [#xB62-#xB63] - - [#xB82] - - [#xBC0] - - [#xBCD] - - [#xC00] - - [#xC04] - - [#xC3C] - - [#xC3E-#xC40] - - [#xC46-#xC48] - - [#xC4A-#xC4D] - - [#xC55-#xC56] - - [#xC62-#xC63] - - [#xC81] - - [#xCBC] - - [#xCBF] - - [#xCC6] - - [#xCCC-#xCCD] - - [#xCE2-#xCE3] - - [#xD00-#xD01] - - [#xD3B-#xD3C] - - [#xD41-#xD44] - - [#xD4D] - - [#xD62-#xD63] - - [#xD81] - - [#xDCA] - - [#xDD2-#xDD4] - - [#xDD6] - - [#xE31] - - [#xE34-#xE3A] - - [#xE47-#xE4E] - - [#xEB1] - - [#xEB4-#xEBC] - - [#xEC8-#xECE] - - [#xF18-#xF19] - - [#xF35] - - [#xF37] - - [#xF39] - - [#xF71-#xF7E] - - [#xF80-#xF84] - - [#xF86-#xF87] - - [#xF8D-#xF97] - - [#xF99-#xFBC] - - [#xFC6] - - [#x102D-#x1030] - - [#x1032-#x1037] - - [#x1039-#x103A] - - [#x103D-#x103E] - - [#x1058-#x1059] - - [#x105E-#x1060] - - [#x1071-#x1074] - - [#x1082] - - [#x1085-#x1086] - - [#x108D] - - [#x109D] - - [#x135D-#x135F] - - [#x1712-#x1714] - - [#x1732-#x1733] - - [#x1752-#x1753] - - [#x1772-#x1773] - - [#x17B4-#x17B5] - - [#x17B7-#x17BD] - - [#x17C6] - - [#x17C9-#x17D3] - - [#x17DD] - - [#x180B-#x180D] - - [#x180F] - - [#x1885-#x1886] - - [#x18A9] - - [#x1920-#x1922] - - [#x1927-#x1928] - - [#x1932] - - [#x1939-#x193B] - - [#x1A17-#x1A18] - - [#x1A1B] - - [#x1A56] - - [#x1A58-#x1A5E] - - [#x1A60] - - [#x1A62] - - [#x1A65-#x1A6C] - - [#x1A73-#x1A7C] - - [#x1A7F] - - [#x1AB0-#x1ABD] - - [#x1ABF-#x1ACE] - - [#x1B00-#x1B03] - - [#x1B34] - - [#x1B36-#x1B3A] - - [#x1B3C] - - [#x1B42] - - [#x1B6B-#x1B73] - - [#x1B80-#x1B81] - - [#x1BA2-#x1BA5] - - [#x1BA8-#x1BA9] - - [#x1BAB-#x1BAD] - - [#x1BE6] - - [#x1BE8-#x1BE9] - - [#x1BED] - - [#x1BEF-#x1BF1] - - [#x1C2C-#x1C33] - - [#x1C36-#x1C37] - - [#x1CD0-#x1CD2] - - [#x1CD4-#x1CE0] - - [#x1CE2-#x1CE8] - - [#x1CED] - - [#x1CF4] - - [#x1CF8-#x1CF9] - - [#x1DC0-#x1DFF] - - [#x20D0-#x20DC] - - [#x20E1] - - [#x20E5-#x20F0] - - [#x2CEF-#x2CF1] - - [#x2D7F] - - [#x2DE0-#x2DFF] - - [#x302A-#x302D] - - [#x3099-#x309A] - - [#xA66F] - - [#xA674-#xA67D] - - [#xA69E-#xA69F] - - [#xA6F0-#xA6F1] - - [#xA802] - - [#xA806] - - [#xA80B] - - [#xA825-#xA826] - - [#xA82C] - - [#xA8C4-#xA8C5] - - [#xA8E0-#xA8F1] - - [#xA8FF] - - [#xA926-#xA92D] - - [#xA947-#xA951] - - [#xA980-#xA982] - - [#xA9B3] - - [#xA9B6-#xA9B9] - - [#xA9BC-#xA9BD] - - [#xA9E5] - - [#xAA29-#xAA2E] - - [#xAA31-#xAA32] - - [#xAA35-#xAA36] - - [#xAA43] - - [#xAA4C] - - [#xAA7C] - - [#xAAB0] - - [#xAAB2-#xAAB4] - - [#xAAB7-#xAAB8] - - [#xAABE-#xAABF] - - [#xAAC1] - - [#xAAEC-#xAAED] - - [#xAAF6] - - [#xABE5] - - [#xABE8] - - [#xABED] - - [#xFB1E] - - [#xFE00-#xFE0F] - - [#xFE20-#xFE2F] - - -
- -
Mn       ::= [#x300-#x36F#x483-#x487#x591-#x5BD#x5BF#x5C1-#x5C2#x5C4-#x5C5#x5C7#x610-#x61A#x64B-#x65F#x670#x6D6-#x6DC#x6DF-#x6E4#x6E7-#x6E8#x6EA-#x6ED#x711#x730-#x74A#x7A6-#x7B0#x7EB-#x7F3#x7FD#x816-#x819#x81B-#x823#x825-#x827#x829-#x82D#x859-#x85B#x898-#x89F#x8CA-#x8E1#x8E3-#x902#x93A#x93C#x941-#x948#x94D#x951-#x957#x962-#x963#x981#x9BC#x9C1-#x9C4#x9CD#x9E2-#x9E3#x9FE#xA01-#xA02#xA3C#xA41-#xA42#xA47-#xA48#xA4B-#xA4D#xA51#xA70-#xA71#xA75#xA81-#xA82#xABC#xAC1-#xAC5#xAC7-#xAC8#xACD#xAE2-#xAE3#xAFA-#xAFF#xB01#xB3C#xB3F#xB41-#xB44#xB4D#xB55-#xB56#xB62-#xB63#xB82#xBC0#xBCD#xC00#xC04#xC3C#xC3E-#xC40#xC46-#xC48#xC4A-#xC4D#xC55-#xC56#xC62-#xC63#xC81#xCBC#xCBF#xCC6#xCCC-#xCCD#xCE2-#xCE3#xD00-#xD01#xD3B-#xD3C#xD41-#xD44#xD4D#xD62-#xD63#xD81#xDCA#xDD2-#xDD4#xDD6#xE31#xE34-#xE3A#xE47-#xE4E#xEB1#xEB4-#xEBC#xEC8-#xECE#xF18-#xF19#xF35#xF37#xF39#xF71-#xF7E#xF80-#xF84#xF86-#xF87#xF8D-#xF97#xF99-#xFBC#xFC6#x102D-#x1030#x1032-#x1037#x1039-#x103A#x103D-#x103E#x1058-#x1059#x105E-#x1060#x1071-#x1074#x1082#x1085-#x1086#x108D#x109D#x135D-#x135F#x1712-#x1714#x1732-#x1733#x1752-#x1753#x1772-#x1773#x17B4-#x17B5#x17B7-#x17BD#x17C6#x17C9-#x17D3#x17DD#x180B-#x180D#x180F#x1885-#x1886#x18A9#x1920-#x1922#x1927-#x1928#x1932#x1939-#x193B#x1A17-#x1A18#x1A1B#x1A56#x1A58-#x1A5E#x1A60#x1A62#x1A65-#x1A6C#x1A73-#x1A7C#x1A7F#x1AB0-#x1ABD#x1ABF-#x1ACE#x1B00-#x1B03#x1B34#x1B36-#x1B3A#x1B3C#x1B42#x1B6B-#x1B73#x1B80-#x1B81#x1BA2-#x1BA5#x1BA8-#x1BA9#x1BAB-#x1BAD#x1BE6#x1BE8-#x1BE9#x1BED#x1BEF-#x1BF1#x1C2C-#x1C33#x1C36-#x1C37#x1CD0-#x1CD2#x1CD4-#x1CE0#x1CE2-#x1CE8#x1CED#x1CF4#x1CF8-#x1CF9#x1DC0-#x1DFF#x20D0-#x20DC#x20E1#x20E5-#x20F0#x2CEF-#x2CF1#x2D7F#x2DE0-#x2DFF#x302A-#x302D#x3099-#x309A#xA66F#xA674-#xA67D#xA69E-#xA69F#xA6F0-#xA6F1#xA802#xA806#xA80B#xA825-#xA826#xA82C#xA8C4-#xA8C5#xA8E0-#xA8F1#xA8FF#xA926-#xA92D#xA947-#xA951#xA980-#xA982#xA9B3#xA9B6-#xA9B9#xA9BC-#xA9BD#xA9E5#xAA29-#xAA2E#xAA31-#xAA32#xAA35-#xAA36#xAA43#xAA4C#xAA7C#xAAB0#xAAB2-#xAAB4#xAAB7-#xAAB8#xAABE-#xAABF#xAAC1#xAAEC-#xAAED#xAAF6#xABE5#xABE8#xABED#xFB1E#xFE00-#xFE0F#xFE20-#xFE2F]
-
- Referenced by: -
- - -====================================================================================================================== -Nd -====================================================================================================================== - - -.. raw:: html - - - - - - [0-9] - - [#x660-#x669] - - [#x6F0-#x6F9] - - [#x7C0-#x7C9] - - [#x966-#x96F] - - [#x9E6-#x9EF] - - [#xA66-#xA6F] - - [#xAE6-#xAEF] - - [#xB66-#xB6F] - - [#xBE6-#xBEF] - - [#xC66-#xC6F] - - [#xCE6-#xCEF] - - [#xD66-#xD6F] - - [#xDE6-#xDEF] - - [#xE50-#xE59] - - [#xED0-#xED9] - - [#xF20-#xF29] - - [#x1040-#x1049] - - [#x1090-#x1099] - - [#x17E0-#x17E9] - - [#x1810-#x1819] - - [#x1946-#x194F] - - [#x19D0-#x19D9] - - [#x1A80-#x1A89] - - [#x1A90-#x1A99] - - [#x1B50-#x1B59] - - [#x1BB0-#x1BB9] - - [#x1C40-#x1C49] - - [#x1C50-#x1C59] - - [#xA620-#xA629] - - [#xA8D0-#xA8D9] - - [#xA900-#xA909] - - [#xA9D0-#xA9D9] - - [#xA9F0-#xA9F9] - - [#xAA50-#xAA59] - - [#xABF0-#xABF9] - - [#xFF10-#xFF19] - - -
- -
Nd       ::= [0-9#x660-#x669#x6F0-#x6F9#x7C0-#x7C9#x966-#x96F#x9E6-#x9EF#xA66-#xA6F#xAE6-#xAEF#xB66-#xB6F#xBE6-#xBEF#xC66-#xC6F#xCE6-#xCEF#xD66-#xD6F#xDE6-#xDEF#xE50-#xE59#xED0-#xED9#xF20-#xF29#x1040-#x1049#x1090-#x1099#x17E0-#x17E9#x1810-#x1819#x1946-#x194F#x19D0-#x19D9#x1A80-#x1A89#x1A90-#x1A99#x1B50-#x1B59#x1BB0-#x1BB9#x1C40-#x1C49#x1C50-#x1C59#xA620-#xA629#xA8D0-#xA8D9#xA900-#xA909#xA9D0-#xA9D9#xA9F0-#xA9F9#xAA50-#xAA59#xABF0-#xABF9#xFF10-#xFF19]
-
- Referenced by: -
- - -====================================================================================================================== -Pc -====================================================================================================================== - - -.. raw:: html - - - - - - [#x203F-#x2040] - - [#x2054] - - [#xFE33-#xFE34] - - [#xFE4D-#xFE4F] - - [#xFF3F] - - -
- -
Pc       ::= [#x203F-#x2040#x2054#xFE33-#xFE34#xFE4D-#xFE4F#xFF3F]
-
- Referenced by: -
- - -====================================================================================================================== -CJK -====================================================================================================================== - - -.. raw:: html - - - - - - [#xAC00-#xD7A3] - - [#x4E00-#x9FFF] - - -
- -
CJK      ::= [#xAC00-#xD7A3#x4E00-#x9FFF]
-
- - -====================================================================================================================== -ESC -====================================================================================================================== - - -.. raw:: html - - - - - - \ - - n - - t - - b - - r - - f - - \ - - " - - -
- -
ESC      ::= '\' [ntbrf\"]
-
- Referenced by: -
- - -====================================================================================================================== -S_CHAR_LITERAL -====================================================================================================================== - - -.. raw:: html - - - - - - U - - E - - N - - R - - B - - RB - - _utf8 - - q'{ - - . - - }' - - ' - - ESC - \' - - [^'\] - - '' - - [^'] - - ' - - q'( - - . - - )' - - q'[ - - . - - ]' - - q'' - - . - - '' - - -
- - -
         ::= ( [UENRB] | 'RB' | '_utf8' )? ( "'" ( ( ESC | "\'" | [^'\] )* | ( "''" | [^'] )+ ) "'" | "q'{" .* "}'" | "q'(" .* ")'" | "q'[" .* "]'" | "q''" .* "''" )
-
- - -====================================================================================================================== -S_QUOTED_IDENTIFIER -====================================================================================================================== - - -.. raw:: html - - - - - - " - - "" - - [^"#xA#xD] - - " - - ` - - [^`#xA#xD] - - ` - - [ - - [^#x5D#xA#xD] - - ] - - -
- - -
         ::= '"' ( '""' | [^"#xA#xD] )* '"'
-
           | '`' [^`#xA#xD]+ '`'
-
           | '[' [^#x5D#xA#xD]* ']'
-
- - -====================================================================================================================== -EOF -====================================================================================================================== - - -.. raw:: html - - - - - - $ - - -
- -
EOF      ::= $
-
- Referenced by: -
- - \ No newline at end of file diff --git a/src/site/sphinx/unsupported.rst b/src/site/sphinx/unsupported.rst index c231dbe814..f81ab87d3e 100644 --- a/src/site/sphinx/unsupported.rst +++ b/src/site/sphinx/unsupported.rst @@ -3,30 +3,86 @@ Unsupported Grammar of various RDBMS *************************************** *JSQLParser* is a RDBMS agnostic parser with a certain focus on SQL:2016 Standard compliant Queries and the "Big Four" (Oracle, MS SQL Server, Postgres, MySQL/MariaDB). -We would like to recommend writing portable, standard compliant SQL in general. +We would like to recommend writing portable, standard compliant SQL in general. -- Oracle PL/SQL blocks are not support. - - .. code-block:: sql +Missing syntax is added on demand — please `open an issue `_ with a minimal example. - DECLARE - num NUMBER; - BEGIN - num := 10; - dbms_output.put_line('The number is ' || num); - END; -- DDL statements +Procedural SQL +======================================= - While *JSQLParser* provides a lot of generic support for DDL statements, it is possible that certain RDBMS specific syntax (especially about indices, encodings, compression) won't be supported. +Procedural-language support is partial. Accepting a block or routine definition does not necessarily mean that its body is parsed into a statement tree. Select the relevant dialect as described in :ref:`Choose a Dialect`. -- Interval Operators +.. code-block:: sql + :caption: Oracle anonymous block — supported with Dialect.ORACLE - Anything like `DAY HOUR MINUTE SECOND [TO HOUR MINUTE SECOND]` is not supported.: + DECLARE + num NUMBER; + BEGIN + num := 10; + dbms_output.put_line('The number is ' || num); + END; - .. code-block:: sql +.. code-block:: postgresql + :caption: PostgreSQL DO — supported with Dialect.POSTGRESQL; body remains opaque - values cast ((time '12:03:34' - time '11:57:23') minute to second as varchar(8)); + DO $$ + BEGIN + RAISE NOTICE 'hello'; + END + $$; +The PostgreSQL example produces a ``DoStatement`` whose ``getCode()`` is a ``StringValue``. The body, quotes and dollar tag are preserved, and statements following the block are parsed separately. PL/pgSQL declarations, conditions and statements inside that literal are not exposed as child AST nodes. Table discovery therefore rejects the opaque body rather than reporting an incomplete table list. +Full procedural-language coverage, including cursors, loops and ``ELSIF``, remains outside the supported subset. +What *is* supported: + +- ``BEGIN .. END`` blocks and ``IF .. ELSE`` around ordinary statements (``Block``, ``IfElseStatement``) +- ``DECLARE @variable`` in the T-SQL sense (``DeclareStatement``) +- PostgreSQL ``DO`` wrappers with the body preserved as a string literal (``DoStatement``) +- Selected Oracle anonymous blocks, including variable declarations, assignments and exception handlers (``OracleBlock``); see :ref:`Oracle anonymous blocks` + +Routine and trigger definitions +--------------------------------------- + +``CREATE FUNCTION`` and ``CREATE PROCEDURE`` **are** accepted, but the body is captured as an opaque token list rather than parsed into a tree. You get a ``CreateFunction`` / ``CreateProcedure`` that round-trips to the original text — you cannot traverse or rewrite what is inside it. + +``CREATE TRIGGER`` follows the MySQL shape (``FOR EACH ROW`` with a single body statement). + +.. note:: + + Both are reported as ``OPAQUE`` by :ref:`Classify a Statement` — the analysis will not claim a routine body is side-effect free, because nothing about it is knowable. + + +Query features not implemented +======================================= + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Feature + - Notes + * - ``MATCH_RECOGNIZE`` + - SQL:2016 row pattern recognition (Oracle, Snowflake, Flink, Trino) + * - Oracle ``MODEL`` clause + - spreadsheet-style inter-row calculation + + + +Dialect-specific syntax +======================================= + +The remaining gaps are narrow: one product's spelling of one construct. The `open issues `_ are the accurate list at any moment — recurring themes are Informix and Teradata extensions, T-SQL ``FOR XML PATH`` and table variables, and assorted vendor-specific DDL options. + +If you hit one, you do not have to abandon the parse: + +.. code-block:: java + + Statements statements = CCJSqlParserUtil.parseStatements( + sqlStr, parser -> parser.withUnsupportedStatements() ); + +The offending statement comes back as an ``UnsupportedStatement`` holding its original text, and the rest of the script parses normally. See :ref:`Handle Parse Errors`. + +Before concluding something is unsupported, check the parser features: square brackets, backslash escapes, double-quoted strings and hash comments are all **off by default** and are the most common cause of a "not supported" report that is really a dialect setting. See :ref:`Choose a Dialect`. diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index c0bcdac88a..250dda5b6f 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -2,44 +2,105 @@ How to use it ****************************** -.. hint:: +JSQLParser turns SQL text into a tree of Java objects, lets you inspect or rewrite that tree, and prints it back out as SQL. Everything on this page is built on those three moves. - 1) **Quoting:** Double Quotes ``".."`` are used for quoting identifiers. Parsing T-SQL on **MS SQL Server** or **Sybase** with Squared Brackets ``[..]`` depends on ``Squared Bracket Quotation`` as shown in section :ref:`Define the Parser Features` below. +.. code-block:: java + :caption: The whole idea in five lines + + Statement statement = CCJSqlParserUtil.parse("SELECT a FROM my_table WHERE id = 42"); + + // inspect it + PlainSelect select = (PlainSelect) statement; + Table table = (Table) select.getFromItem(); // my_table + + // print it back + String sql = statement.toString(); + +.. tip:: + + **New here?** Read :ref:`Add JSQLParser to your Project`, then :ref:`Parse a SQL Statement`, then :ref:`Explore the Parsed Tree`. Everything after that is optional and can be read in any order. + +.. list-table:: What is on this page + :header-rows: 1 + :widths: 30 70 + + * - Section + - Use it when you want to … + * - :ref:`Add JSQLParser to your Project` + - pull in the dependency, and pick between the Manticore and upstream builds + * - :ref:`Parse a SQL Statement` + - turn SQL text into Java objects + * - :ref:`Explore the Parsed Tree` + - find your way around the object model + * - :ref:`Classify a Statement` + - know whether SQL reads, writes or returns rows — before you run it + * - :ref:`Find Table Names` + - list every table a statement touches + * - :ref:`Use the Visitor Patterns` + - walk the whole tree and react to specific nodes + * - :ref:`Build a SQL Statement` + - construct SQL from Java instead of from text + * - :ref:`Handle Parse Errors` + - keep going when one statement in a script is broken + * - :ref:`Choose a Dialect` + - parse T-SQL brackets, MySQL escapes, BigQuery quoting … + * - :ref:`Compile from Source Code` + - build JSQLParser yourself or contribute + + +Add JSQLParser to your Project +============================== - 2) JSQLParser uses a more restrictive list of ``Reserved Keywords`` and such keywords will **need to be quoted**. +There are two sets of artifacts on Maven Central, built from the same source under the same dual licence: - 3) **Escaping:** JSQLParser pre-defines standard compliant **Single Quote** ``'..`` **Escape Character**. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature. See section :ref:`Define the Parser Features` below for details. +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 - 4) Oracle Alternative Quoting is partially supported for common brackets such as ``q'{...}'``, ``q'[...]'``, ``q'(...)'`` and ``q''...''``. + * - Artifact + - ``groupId`` + - Cut from + * - **Manticore build** (recommended) + - ``com.manticore-projects.jsqlformatter`` + - the current development line, released continuously + * - Upstream release + - ``com.github.jsqlparser`` + - the official release cadence + * - Upstream snapshot + - ``com.github.jsqlparser`` + - the latest commit, overwritten in place - 5) Supported Statement Separators are Semicolon ``;``, ``GO``, Slash ``/`` or two empty lines ``\n\n\n``. +Upstream releases are cut infrequently. Between two of them a lot of grammar and performance work lands — the 11× parse speed-up, JavaCC 8 support, new dialect syntax — and waiting for the next official version to catch up can mean months on a build that already has the fix you need. +Snapshots are not the answer either: a ``-SNAPSHOT`` coordinate is mutable, so the same version string can resolve to different bytes tomorrow. That is fine for trying something out and wrong for a reproducible build. -Compile from Source Code -============================== - -You will need to have ``JDK 8`` or ``JDK 11`` installed. Please note that JSQLParser-4.9 is the last ``JDK 8`` compatible release and all development after will depend on ``JDK 11``. Building JSQLParser-5.1 and newer with Gradle will depend on a JDK17 toolchain due to the used plugins. +The **Manticore builds** fill that gap. Each one is an immutable, versioned release published to Maven Central from the current development line, so you get the fixes early *and* a build that stays reproducible. Use them unless you have a reason to pin to the official release — and note the different ``groupId``, the artifact name is the same. -.. tab:: Maven +.. tab:: Maven — Manticore - .. code-block:: shell + .. code-block:: xml - git clone --depth 1 https://github.com/JSQLParser/JSqlParser.git - cd JSqlParser - mvn install + + com.manticore-projects.jsqlformatter + jsqlparser + [5.3.218,) + -.. tab:: Gradle + The range ``[5.3.218,)`` takes the newest available build. Pin an exact version instead once you ship. - .. code-block:: shell +.. tab:: Gradle — Manticore - git clone --depth 1 https://github.com/JSQLParser/JSqlParser.git - cd JSqlParser - gradle publishToMavenLocal + .. code-block:: groovy + repositories { + mavenCentral() + } + dependencies { + implementation 'com.manticore-projects.jsqlformatter:jsqlparser:+' + } -Build Dependencies -============================== + ``+`` takes the newest available build. Pin an exact version instead once you ship. .. tab:: Maven Release @@ -100,11 +161,15 @@ Build Dependencies implementation 'com.github.jsqlparser:jsqlparser:|JSQLPARSER_SNAPSHOT_VERSION|' } +.. note:: + + Features documented here may reach the Manticore builds before the next upstream release. If a class or method on this page is missing, check which of the two you are resolving. + Parse a SQL Statement -============================== +============================== -Parse the SQL Text into Java Objects: +``CCJSqlParserUtil.parse()`` is the entry point. It returns a ``Statement``, which you cast to the concrete type you expect. .. code-block:: java @@ -127,8 +192,27 @@ Parse the SQL Text into Java Objects: Assertions.assertEquals("a", a.getColumnName()); Assertions.assertEquals("b", b.getColumnName()); +For several statements at once, use ``CCJSqlParserUtil.parseStatements()``, which returns a ``Statements`` — an ``ArrayList``. + +.. code-block:: java -For guidance with the API, use `JSQLFormatter `_ to visualize the Traversable Tree of Java Objects: + Statements script = CCJSqlParserUtil.parseStatements( + "UPDATE t SET a = 1; SELECT a FROM t;"); + + assertEquals(2, script.size()); + +.. note:: + + Supported statement separators are semicolon ``;``, ``GO``, slash ``/`` and two empty lines ``\n\n\n``. + Use ``withBlankLinesAsSeparator(false)`` to treat empty lines as plain whitespace instead. + +If parsing fails on syntax JSQLParser does not know, see :ref:`Handle Parse Errors` — and please `open an issue `_, missing syntax gets added on demand. + + +Explore the Parsed Tree +============================== + +The fastest way to learn the object model is to look at it. Paste your SQL into `JSQLFormatter `_ and it will draw the tree, with the Java class of every node: .. raw:: html @@ -136,7 +220,7 @@ For guidance with the API, use `JSQLFormatter SQL Text └─Statements: net.sf.jsqlparser.statement.select.Select - ├─selectItems -> Collection + ├─selectItems -> Collection<SelectItem> │ └─LongValue: 1 ├─Table: dual └─where: net.sf.jsqlparser.expression.operators.relational.EqualsTo @@ -145,48 +229,1141 @@ For guidance with the API, use `JSQLFormatter -Error Handling +Read that as a map: each line is a getter away. ``select.getSelectItems()``, ``select.getFromItem()``, ``select.getWhere()``. Once the tree gets deeper than a couple of levels, stop casting by hand and use :ref:`Use the Visitor Patterns`. + + +PostgreSQL CHECK inheritance +---------------------------- + +With ``Dialect.POSTGRESQL``, CREATE and ALTER CHECK constraints support +``NO INHERIT``. ``CheckConstraint.isNoInherit()`` reports this flag; use +``setNoInherit`` or ``withNoInherit`` to change it. The check expression remains +an editable ``Expression`` visited by the existing table traversal and deparsers. + +For ``ALTER TABLE t ADD CHECK (id > 0) NO INHERIT NOT VALID``, the two options +are independent: ``isNoInherit()`` is true and +``getConstraintAttributes().isNotValid()`` is true. Clearing one flag preserves +the other when the statement is rendered. + +DROP targets +------------ + +``Drop`` keeps the parts of a DROP statement that are not plain object names in +typed properties: + +* FUNCTION, PROCEDURE, ROUTINE and AGGREGATE targets are ``RoutineReference`` + items in ``getRoutines()``, the model that GRANT and COMMENT use. Every target + keeps its own signature, so ``DROP PROCEDURE p(int)`` and + ``DROP FUNCTION f(int), g(text)`` render as written. Null arguments mean an + omitted signature; an empty list is an explicit ``()``. +* USER, ROLE and GROUP targets and the roles of PostgreSQL ``DROP OWNED BY`` are + ``UserAccount`` items in ``getAccounts()``, read with the MySQL account-name rule + that CREATE USER uses. MySQL accounts keep their host, as in ``'app'@'%'``, + and a quoted PostgreSQL role name such as ``"Admin"`` keeps ``"`` in + ``getQuoteStr()``. +* ``DROP TRIGGER t ON tbl`` and ``DROP RULE r ON tbl`` expose ``tbl`` through + ``getTable()``, like ``DROP INDEX i ON tbl``, and table discovery reports it. + SQL Server's ``ON DATABASE`` and ``ON ALL SERVER`` trigger scopes stay + parameters. +* ``getBehavior()`` holds an explicit CASCADE or RESTRICT as a + ``CascadeBehavior``. It is rendered last. +* ``isUndo()``, ``getEngine()`` and ``isEngineUseEquals()`` cover MySQL + ``DROP [UNDO] TABLESPACE name [ENGINE [=] engine]``. ``isForce()`` covers + PostgreSQL ``DROP DATABASE name [WITH] (FORCE)``; the optional WITH is + always rendered. +* ``getAccessMethod()`` holds the index access method of PostgreSQL + ``DROP OPERATOR CLASS|FAMILY name USING method``, which takes one name. + ``isProcedural()`` records the ``PROCEDURAL LANGUAGE`` spelling. + +TYPE, DOMAIN, EVENT, EVENT TRIGGER, TABLESPACE, USER and GROUP are recognized +object types, and so are PostgreSQL's ACCESS METHOD, LANGUAGE, OPERATOR CLASS, +OPERATOR FAMILY and TEXT SEARCH CONFIGURATION, DICTIONARY, PARSER and TEMPLATE. +``getType()`` returns their words separated by spaces, as in ``ACCESS METHOD``. PostgreSQL ``REASSIGN OWNED BY old_role [, ...] TO new_role`` is +parsed into ``ReassignOwned``, whose roles use the same ``UserAccount`` model. + +.. code-block:: java + + Drop drop = (Drop) CCJSqlParserUtil.parse("DROP FUNCTION f(int), g(text) CASCADE", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + RoutineReference g = drop.getRoutines().get(1); + g.getArguments().get(0).setDataType(new ColDataType("varchar")); + // DROP FUNCTION f(int), g(varchar) CASCADE + String sql = drop.toString(); + +The legacy accessors remain and are derived from these properties: + +* ``getName()`` and ``getNames()`` return read-only snapshots of routine and + account names. Edit ``getRoutines()`` or ``getAccounts()`` to change them. +* ``getTypeToParameters()`` and ``getParamsByType()`` return a read-only map with + the argument declarations of the last routine, where the legacy + single-signature form kept them. The map now also covers PROCEDURE, ROUTINE + and AGGREGATE. +* ``getParameters()`` is a snapshot that includes ON with the owner table, the + MySQL ALGORITHM and LOCK clauses and the CASCADE or RESTRICT keyword, in + source order. ``setParameters()`` replaces the clause with raw tokens and + clears all three; ``addParameters()`` keeps them. Raw tokens are not parsed: + ``addParameters("CASCADE")`` leaves ``getBehavior()`` null. +* ``setName()``, ``setNames()``, ``addNames()`` and ``setTypeToParameters()`` + first convert routine and account targets to the legacy names and single + signature, so ``setName()`` on ``DROP FUNCTION f(int)`` still renders + ``DROP FUNCTION g(int)``. Use ``getRoutines()`` to keep several signatures. + +Migration: mutating the map returned by ``getTypeToParameters()`` for a parsed +routine now throws ``UnsupportedOperationException``; edit its +``RoutineReference`` instead. ``getObjectType()`` reports the new ``AGGREGATE``, +``DOMAIN``, ``EVENT_TRIGGER``, ``GROUP``, ``OWNED``, ``ROLE``, ``ROUTINE``, +``RULE``, ``TYPE`` and ``USER`` kinds where it reported ``OTHER``. +``DROP OWNED BY r`` no longer returns the name ``BY``. + +DROP INDEX owners +----------------- + +``DROP INDEX ix ON app.t`` exposes ``app.t`` through ``Drop.getTable()``; +``getName()`` continues to identify the index. The owner is populated for the +single-index ON form used by MySQL and SQL Server. A DROP INDEX without ON, +as in PostgreSQL, has a null owner: resolving the index's table requires a catalog. +SQL Server's multi-owner list and WITH options are outside this extension. + +``getParameters()`` includes ON and the rendered table as a legacy token snapshot +when a structured owner exists. Mutate ``getTable()`` or use ``setTable()`` to +update the owner; ``addParameters()`` appends options without losing it. +``setTable(null)`` removes ON, while ``setParameters()`` replaces the entire +parameter clause with raw tokens and clears the structured owner. Raw parameter +setters do not parse SQL or infer a table. + +Table discovery and statement visitors traverse DROP TABLE/VIEW targets and +explicit index owners, without reporting catalog-only object names as tables. +The statement deparser delegates real tables to its configured select deparser. +MySQL ALGORITHM and LOCK clauses retain their order and optional equals signs as +typed nodes; see :ref:`Share MySQL index options`. + +MySQL named constraint drops +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Without a dialect and with ``Dialect.MYSQL``, ``DROP FOREIGN KEY name`` and +``DROP CHECK name`` expose the target through ``AlterExpression.getConstraintName()``. +``Dialect.MARIADB`` accepts the named FOREIGN KEY form; MariaDB has no ``DROP CHECK``. +Other dialects drop constraints with ``DROP CONSTRAINT``. A parenthesized +``DROP FOREIGN KEY (...)`` keeps the legacy column-list form. Inspect +``getOperation()`` for ``DROP_FOREIGN_KEY`` or ``DROP_CHECK``. Identifier +quotes are preserved, and changing the name changes both SQL renderers. +These named targets do not populate the legacy ``getPkColumns()`` list. + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE child DROP FOREIGN KEY fk_parent", + parser -> parser.withDialect(Dialect.MYSQL)); + AlterExpression drop = alter.getAlterExpressions().get(0); + drop.setConstraintName("fk_customer"); + // ALTER TABLE child DROP FOREIGN KEY fk_customer + String sql = alter.toString(); + + // The same API also supports constructing an action without parsing SQL. + AlterExpression checkDrop = new AlterExpression() + .withOperation(AlterOperation.DROP_CHECK).withConstraintName("positive_id"); + +Edit table and view column renames +---------------------------------- + +``AlterExpression.getColumnRename()`` exposes a ``ColumnRenameAction`` with +``getSourceName()`` and ``getTargetName()``. Their setters update the existing +AST directly, preserving the optional ``COLUMN`` keyword. Names include their +written identifier quotes; include quotes in replacement names when needed. + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE accounts RENAME COLUMN old_id TO new_id", + parser -> parser.withDialect(Dialect.MYSQL)); + ColumnRenameAction rename = alter.getAlterExpressions().get(0).getColumnRename(); + rename.getSourceName(); // old_id + rename.setTargetName("customer_id"); + // ALTER TABLE accounts RENAME COLUMN old_id TO customer_id + String sql = alter.toString(); + +Import ``ColumnRenameAction`` from ``net.sf.jsqlparser.statement.alter``. +PostgreSQL table renames of columns use the same API. With ``Dialect.POSTGRESQL``, +column renames on views and materialized views expose it through the entries in +``AlterRelation.getActions()``. The lookup returns ``null`` for other operations, +including renaming the table, view, index or constraint itself. Existing +action-specific getters and setters remain available and see the same changes. + +Shared CASCADE and RESTRICT behavior +------------------------------------ + +``AlterDomain``, ``AlterType`` (including ``AttributeChange``), ``DropPolicy``, +``Truncate`` and ``Revoke`` use ``net.sf.jsqlparser.statement.CascadeBehavior`` +for their explicit ``CASCADE`` and ``RESTRICT`` clauses. A null value preserves +an omitted clause; it does not insert an explicit ``RESTRICT``. + +.. code-block:: java + + import net.sf.jsqlparser.statement.CascadeBehavior; + + truncate.setDropBehavior(CascadeBehavior.RESTRICT); + revoke.setBehavior(truncate.getDropBehavior()); + revoke.setBehavior(null); // Omit the clause and leave the server default unchanged. + +Migration: the nested enums ``AlterDomain.Behavior``, ``AlterType.Behavior``, +``DropPolicy.Behavior``, ``Truncate.DropBehavior`` and ``Revoke.Behavior`` have +been removed. Replace their imports and enum references with ``CascadeBehavior``. +The existing getter and setter names remain, but their return and parameter types +have changed, so clients must recompile. ``Truncate.setCascade(false)`` still +clears the clause. Foreign-key referential actions retain their separate enum. + +``ALTER TABLE`` drop actions expose the same enum through +``AlterExpressionDrop.getBehavior()``, for example in +``ALTER TABLE t DROP COLUMN IF EXISTS c RESTRICT`` and +``ALTER TABLE t DROP CONSTRAINT c CASCADE``. Oracle's ``CASCADE CONSTRAINTS`` and +``INVALIDATE`` remain raw parameters. ``getParameters()`` still ends with the +behavior keyword; when a behavior is set, it returns a read-only snapshot, so use +``setBehavior`` to change the clause. + +Edit shared table options +------------------------- + +``CreateTable.getTableOptions()`` and +``AlterExpressionTableOption.getStructuredTableOption()`` use the same mutable +``TableOption`` model. Structured values and opaque tokens are alternative +representations. ``setTokens(nonNullList)`` discards the previous kind, name, +value and child nodes: the option becomes ``Kind.OTHER`` without parsing the +tokens. Raw token lists remain mutable; ``getTokens()`` on a structured option +returns an unmodifiable snapshot. + +.. code-block:: java + + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT) ENGINE=InnoDB"); + TableOption option = table.getTableOption(TableOption.Kind.ENGINE).orElseThrow(); + option.setTokens(List.of("ROW_FORMAT", "=", "DYNAMIC")); + table.getTableOption(TableOption.Kind.ENGINE).isEmpty(); // true + + // Supply structured properties explicitly; the old tokens are discarded. + option.withKind(TableOption.Kind.COMMENT).withName("COMMENT") + .withValue("'new'").withUseEquals(true); + // CREATE TABLE t (id INT) COMMENT = 'new' + +Any structured setter leaves the opaque representation; it never recovers +previous values from those tokens. ``setTokens(null)`` clears opaque tokens +without restoring discarded properties, and leaves structured options alone. +``setUnionTables``, ``setStorageParameters``, ``setForeignTableOptions`` and +``setOnCommitAction`` replace the current contents with their respective +structured payloads. Their lists and child nodes remain editable. An empty +UNION list means ``UNION ()``; +a null payload omits its value. A non-null ``setTablespaceStorage`` also selects +TABLESPACE; supply its tablespace name with ``setValue``. Choose the new kind and spelling explicitly +when using the scalar setters. These methods edit the AST rather than checking +database-specific DDL validity. + +A temporary table's ``ON COMMIT`` clause and a table access method are single +options. ``Kind.ON_COMMIT`` exposes ``getOnCommitAction()``: ``PRESERVE_ROWS``, +``DELETE_ROWS`` or ``DROP``, plus ``PRESERVE_DEFINITION`` and ``DROP_DEFINITION`` +for Oracle private temporary tables. ``Kind.ACCESS_METHOD`` stores the method of +``USING method`` as its value, including ``CREATE TABLE … USING method AS query``. +Use ``TableOption.onCommit`` and ``TableOption.accessMethod`` to build them. +``getTableOptionsStrings()`` keeps the legacy word tokens, such as ``ON``, +``COMMIT``, ``DELETE`` and ``ROWS``. + +.. code-block:: java + + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TEMP TABLE t (id INT) USING heap ON COMMIT DROP", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + table.getTableOption(TableOption.Kind.ACCESS_METHOD).orElseThrow().getValue(); // heap + table.getTableOption(TableOption.Kind.ON_COMMIT).orElseThrow() + .setOnCommitAction(TableOption.OnCommitAction.PRESERVE_ROWS); + // CREATE TEMP TABLE t (id INT) USING heap ON COMMIT PRESERVE ROWS + +Share MySQL index options +------------------------- + +CREATE INDEX, the index and key definitions of CREATE TABLE, and ALTER TABLE +``ADD`` or ``ALTER INDEX`` parse MySQL index options with one grammar rule into +one model. ``IndexOptions.getSpecOptions()``, also available on ``Index`` and +``KeyConstraint``, returns the options written after the key list as mutable, +ordered ``IndexSpecOption`` nodes. ``getKind()`` selects the payload: + +* ``KEY_BLOCK_SIZE``, ``ENGINE_ATTRIBUTE`` and ``SECONDARY_ENGINE_ATTRIBUTE``: + ``getValue()`` and the optional ``=`` reported by ``isUseEquals()`` +* ``INDEX_TYPE`` (``USING BTREE``) and ``PARSER`` (``WITH PARSER ngram``): + ``getValue()`` +* ``COMMENT``: ``getComment()`` returns the literal as a ``StringValue`` that + records how the dialect escapes it, so ``getNotExcapedValue()`` returns the + stored text, for example ``it's`` for ``'it''s'`` or, with MySQL backslash + escapes, ``'it\'s'``. ``getValue()`` remains the literal's SQL text. +* ``VISIBILITY``: ``getVisible()`` for VISIBLE or INVISIBLE +* ``OTHER``: opaque tokens that CREATE TABLE and CREATE INDEX accept for other + dialects + +Values keep their SQL spelling, including the quotes of string literals. An +index type written before the key list, as in ``KEY k USING BTREE (a)``, +``PRIMARY KEY USING BTREE (id)`` or ``CREATE INDEX k USING HASH ON t (a)``, +remains ``getUsing()``; ``CreateIndex.isIndexTypeBeforeOn()`` still distinguishes +the two CREATE INDEX positions. After the key list it is an ``INDEX_TYPE`` option +at its source position. PostgreSQL's INCLUDE, NULLS DISTINCT, storage parameters +and tablespace keep their dedicated properties. + +CREATE INDEX keeps these options on ``getIndex()``. Its ALGORITHM and LOCK +clauses are ``AlgorithmOrLockOption`` nodes in +``CreateIndex.getAlgorithmOrLockOptions()``; DROP INDEX exposes the same type +through ``Drop.getAlgorithmOrLockOptions()``. CREATE INDEX renders them after the +index options, as MySQL requires. + +.. code-block:: java + + CreateIndex create = (CreateIndex) CCJSqlParserUtil.parse( + "CREATE UNIQUE INDEX uk ON t (a) USING BTREE COMMENT 'x' ALGORITHM = INPLACE", + parser -> parser.withDialect(Dialect.MYSQL)); + List options = create.getIndex().getSpecOptions(); + options.get(0).setValue("HASH"); // INDEX_TYPE + options.add(IndexSpecOption.visibility(false)); + create.getAlgorithmOrLockOptions().get(0).setValue("COPY"); + // CREATE UNIQUE INDEX uk ON t (a) USING HASH COMMENT 'x' INVISIBLE ALGORITHM = COPY + +The kind of an option is fixed. Setters for a payload that the kind does not have, +and ``setTokens`` on a typed option, throw ``IllegalStateException``; replace the +node to change its kind. ``IndexSpecOption.raw`` copies its tokens and never +parses them. + +The legacy accessors are derived from these nodes. ``getIndexSpec()`` returns an +unmodifiable token snapshot of ``getSpecOptions()``, or null when the options are +unset. ``CreateIndex.getTailParameters()`` returns the tokens of the index options +followed by ALGORITHM and LOCK, and ``Drop.getParameters()`` includes ALGORITHM +and LOCK at their source position. ``setIndexSpec`` and +``CreateIndex.setTailParameters`` replace these clauses with one opaque option; +``Drop.setParameters`` also clears ALGORITHM and LOCK. + +Migration: edits of the ``getIndexSpec()`` and ``getTailParameters()`` snapshots +now throw ``UnsupportedOperationException``; edit the nodes or call the setters. +ALTER TABLE key definitions report the same tokens as CREATE TABLE, for example +``KEY_BLOCK_SIZE``, ``=`` and ``8`` instead of the single entry +``KEY_BLOCK_SIZE = 8``. For a constructed statement without trailing clauses, +``getTailParameters()`` returns an empty list instead of null. Typed option +keywords are rendered in upper case. + +MySQL table and partition comments +---------------------------------- + +A table ``COMMENT`` in CREATE TABLE, CREATE TABLE … AS SELECT and ALTER TABLE is a +``StringValue`` in ``TableOption.getComment()``. Partition and subpartition comments, +including those of ALTER TABLE ADD and REORGANIZE PARTITION, are in +``PartitionDefinition.getCommentValue()``. The literal records the escaping of the +parser configuration, so ``getNotExcapedValue()`` returns the text MySQL stores. + +.. code-block:: java + + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT) COMMENT = 'it\\'s' " + + "PARTITION BY HASH (id) (PARTITION p0 COMMENT 'a\\\\b')", + parser -> parser.withDialect(Dialect.MYSQL)); + TableOption option = table.getTableOption(TableOption.Kind.COMMENT).orElseThrow(); + option.getComment().getNotExcapedValue(); // it's + option.getValue(); // 'it\'s' + table.getPartitioning().getPartitionDefinitions().get(0) + .getCommentValue().getNotExcapedValue(); // a\b + +``getValue()``, ``getTokens()``, ``CreateTable.getTableOptionsStrings()``, +``AlterExpressionTableOption.getCommentText()`` and ``PartitionDefinition.getComment()`` +still return the literal as written, and the SQL output is unchanged except that a +string prefix such as ``n'…'`` is printed in upper case, as in other string literals. +The table comment literal is rendered by the expression deparser and visited like a +column COMMENT literal. + +``setComment``, ``withComment`` and the ``TableOption.comment`` factory replace the +option's contents with the literal and select COMMENT. They keep the equals spelling, +and an existing COMMENT also keeps its name; a null literal omits the value. +``setValue`` stores a plain value and ``setTokens`` opaque tokens. Neither is parsed, +so ``getComment()`` then returns null. Selecting another kind keeps the literal's SQL +text as a plain value. A COMMENT value that is not a string literal also stays a plain +value, for example a double-quoted string parsed without the MySQL dialect, where it +is a quoted identifier. + +``PartitionDefinition.setComment(String)`` reads the SQL text of a literal with +standard escaping; pass a ``StringValue`` with another escaping to +``setCommentValue``. + +Inspect PostgreSQL schema statements +------------------------------------ + +PostgreSQL schema clauses extend the existing ``CreateView``, ``CreateTable``, ``Alter`` and ``Sequence`` models. Use their typed properties to inspect the clauses and preserve distinctions between omitted options and explicit values. + +.. code-block:: java + + CreateView view = (CreateView) CCJSqlParserUtil.parse( + "CREATE MATERIALIZED VIEW IF NOT EXISTS account_totals " + + "AS SELECT id FROM accounts WITH NO DATA"); + view.isMaterialized(); // true + view.isIfNotExists(); // true + view.getWithData(); // Boolean.FALSE; null means the clause was omitted + +Ordinary views expose ordered ``ViewOption`` values for ``security_barrier``, ``security_invoker`` and ``check_option``. ``getCheckOption()`` describes the trailing ``WITH CHECK OPTION`` clause: ``null`` means absent, ``DEFAULT`` preserves the bare clause, and ``LOCAL`` and ``CASCADED`` preserve explicit keywords. ``getEffectiveCheckOption()`` resolves the bare clause to ``CASCADED``. Materialized views expose their access method, storage parameters and tablespace separately. See the PostgreSQL documentation for `CREATE VIEW `_ and `CREATE MATERIALIZED VIEW `_. + +.. code-block:: java + + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE accounts_copy (LIKE accounts INCLUDING ALL EXCLUDING INDEXES)"); + LikeClause like = table.getTableElements(LikeClause.class).get(0); + like.getOptions(); // ordered INCLUDING/EXCLUDING clauses + like.isIncluding(LikeClause.OptionKind.DEFAULTS); // true, inherited from ALL + like.isIncluding(LikeClause.OptionKind.INDEXES); // false, overridden by EXCLUDING + +``getTableElements()`` preserves the order of columns, constraints and ``LIKE`` clauses, including multiple source tables. The three legacy nullable ``LikeClause`` getters report explicit options of that kind; ``isIncluding(OptionKind)`` also resolves ``ALL`` in declaration order. A typed table's ``OF`` type is available through ``CreateTable.getOfType()``. + +Table constraints expose their names and ``ConstraintAttributes`` independently from indexes. ``KeyConstraint`` and ``ExcludeConstraint`` expose ``IndexOptions`` for null treatment, INCLUDE columns, storage parameters and tablespace options. Their ``KeyElement`` nodes expose key expressions, ordering and exclusion operators. Column identity clauses are represented by ``ColumnOption.Kind.IDENTITY`` and ``IdentityDefinition``, with a generation mode and ordered ``Sequence.Parameter`` values. These APIs cover the schema clauses described in `CREATE TABLE `_. + +Inspect indexes and constraints separately +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Index`` represents index declarations. Constraints are independent +``NamedConstraint`` nodes: ``KeyConstraint`` for PRIMARY KEY/UNIQUE, +``ForeignKeyConstraint``, ``CheckConstraint``, ``ExcludeConstraint``, +``DefaultConstraint``, ``NotNullConstraint`` and ``ConstraintUsingIndex``. +Constraint nodes do not extend ``Index``. Their ``ConstraintKind`` is separate +from the physical index classification ``Index.Kind``. + +``CreateTable.getIndexes()`` returns only indexes, while +``getTableConstraints()`` returns table-level constraints. Both are mutable +views of ``getTableElements()`` when ordered elements are present; removing a +constraint preserves indexes and their source positions. To inspect column +and table constraints together, use ``getConstraints()``. This returns +``ConstraintDeclaration`` views in declaration order. + +.. code-block:: java + + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT PRIMARY KEY, label TEXT)"); + ConstraintDeclaration primary = table.getConstraints().stream() + .filter(declaration -> declaration.getKind() == ConstraintKind.PRIMARY_KEY) + .findFirst().orElseThrow(); + primary.getColumn().setColumnName("new_id"); + // CREATE TABLE t (new_id INT PRIMARY KEY, label TEXT) + +``getColumn()`` retains the owner of an inline declaration and is null for a +table constraint. ``getColumnOption()`` exposes an inline option; +``getConstraint()`` exposes its original ``NamedConstraint`` when one exists. +Column REFERENCES and nullability options retain their own payloads instead +of manufacturing a table constraint or a key-column list. Edit the original +source nodes to change SQL. Explicit NOT NULL and REFERENCES are classified; +ordinary column DEFAULT values are options, while explicit +``DefaultConstraint`` nodes are classified as DEFAULT. NULL, raw tokens and implied constraints are not +inferred. + +The declaration list is an unmodifiable membership snapshot. Its views read +live source nodes, so type edits change ``getKind()``. Request a new list after +adding, removing or changing column options. Structured constraint nodes +remain included when their kind is OTHER. Without ordered table elements, +manually constructed columns precede table constraints and indexes. + +``ColumnOption.getConstraintKind()`` and +``AlterExpression.getConstraintKind()`` also return ``ConstraintKind``. +``getOperation()`` distinguishes addition from removal. ALTER ADD constraints +use ``getConstraint()``; ADD INDEX uses ``getIndex()``. These getters expose +stored definitions; after changing an action, ``getConstraintKind()`` describes +the active syntax rather than an inactive stored definition. Column actions +retain constraints on their column options. RENAME CONSTRAINT uses +``getConstraintName()`` for the source and ``getNewConstraintName()`` for the +target; it does not manufacture index nodes. A named ``DROP CONSTRAINT pk`` reports +``OTHER`` because SQL alone does not reveal what ``pk`` names. This query does +not validate a definition's completeness or consult the database catalogue. + +MySQL table-level ``UNIQUE``, ``UNIQUE KEY`` and ``UNIQUE INDEX`` declarations +are ``KeyConstraint`` nodes. A standalone ``CREATE UNIQUE INDEX`` remains +``CreateIndex`` with an ``Index`` payload. The parser does not invent backing +index nodes for PRIMARY KEY/UNIQUE constraints. PostgreSQL ``USING INDEX`` +retains the existing index name on ``ConstraintUsingIndex`` rather than +creating key elements or an index definition. + +An ALTER foreign key owns one ``ForeignKeyConstraint`` and its mutable +``ForeignKeyReference``. The legacy ``getFk*``/``setFk*`` accessors and +referential-action helpers delegate to these same nodes, so edits through +either API affect both SQL renderers. + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE child ADD FOREIGN KEY (parent_id) REFERENCES parent (id)"); + AlterExpression action = alter.getAlterExpressions().get(0); + ForeignKeyConstraint foreignKey = (ForeignKeyConstraint) action.getConstraint(); + foreignKey.getReference().getTable().setSchemaName("archive"); + action.setFkSourceTable("parent_history"); + // Both APIs now refer to archive.parent_history. + foreignKey.setReferentialAction(ReferentialAction.Type.DELETE, + ReferentialAction.Action.CASCADE); + +New code should use the structured constraint API. ``getFkColumns()`` returns +a rendered snapshot, like ``getPkColumns()`` and ``getUkColumns()``; use +``setFkColumns()``/``addFkColumns()`` to replace or append names, or edit +``foreignKey.getColumns()`` directly. Referenced-column lists and +referential-action objects are live objects on the shared reference. + +A non-null legacy FK setter creates a foreign-key definition when none is +present. It rejects an existing non-FK constraint or index with +``IllegalStateException`` instead of retaining a second hidden definition; +replace the definition explicitly with ``setConstraint()`` first. Reading, +clearing or removing absent FK properties does not create a definition. +Replacing or clearing ``setConstraint()`` also replaces or clears what the +legacy FK accessors expose. These accessors inspect stored definitions; use +``getOperation()``/``getConstraintKind()`` to identify the active action. + +Migration from the former combined model: + +* Replace table-constraint reads from ``getIndexes()`` with + ``getTableConstraints()`` and ALTER constraint reads from ``getIndex()`` + with ``getConstraint()``. ``ConstraintDeclaration.getIndex()`` is replaced + by ``getConstraint()``. +* Replace ``ForeignKeyIndex`` with ``ForeignKeyConstraint`` and concrete + ``new NamedConstraint()`` construction with the appropriate constraint + class, usually ``KeyConstraint``. ``NamedConstraint`` is now abstract. +* Constraint enum values move from ``Index.Kind`` to ``ConstraintKind``; + ``Index.Kind`` retains only UNIQUE, INDEX, FULLTEXT, SPATIAL and OTHER. +* Shared key and option values are now ``KeyElement`` (formerly + ``Index.ColumnParams``), ``IndexOption`` (formerly ``Index.Option``) and + ``IndexOptions.Clustering`` (formerly ``Index.Clustering``). These are + standalone shared components, not aliases to nested index types. Replace + ``getColumnWithParams()``/``setColumnNamesWithParams()`` with + ``getColumns()``/``setColumns()`` using ``KeyElement`` values. + +``getType()`` preserves the rendered spelling. ``setType()`` refreshes kind +metadata for indexes and key constraints; ``setKind()`` does not rewrite SQL. +Constraints with a fixed renderer, such as CHECK, always report their actual +constraint kind. Replacing an index with a table constraint requires replacing +the AST node; changing an index's type string cannot change its Java type. + +ALTER key definitions use ``getKeyConstraint()`` for typed access to the same +object returned by ``getConstraint()``. PRIMARY KEY and UNIQUE builder setters +also create this canonical definition; they no longer keep separate column, +name or KEY/INDEX state: + +.. code-block:: java + + Alter add = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD UNIQUE KEY uq (id)", + parser -> parser.withDialect(Dialect.MYSQL)); + KeyConstraint key = add.getAlterExpressions().get(0).getKeyConstraint(); + key.setColumns(List.of(new KeyElement("account_id"))); + key.setIndexName("uq_account"); + System.out.println(add); // ALTER TABLE t ADD UNIQUE KEY uq_account (account_id) + +Parenthesized deletion targets have their own ``getDropColumns()`` and +``setDropColumns()`` API. It covers multi-column ``DROP (...)``, ``DROP UNIQUE +(...)`` and the parser's legacy parenthesized ``DROP FOREIGN KEY (...)`` form. +Named constraint drops continue to use ``getConstraintName()``. No key +definition is manufactured for a drop: + +.. code-block:: java + + Alter drop = (Alter) CCJSqlParserUtil.parse("ALTER TABLE t DROP (old_a, old_b)"); + drop.getAlterExpressions().get(0).getDropColumns().set(0, "obsolete_a"); + System.out.println(drop); // ALTER TABLE t DROP (obsolete_a, old_b) + +``setDropColumns`` copies its input; ``getDropColumns`` returns the owned mutable +list. ``withDropColumns`` and ``addDropColumns`` also work before selecting the +operation. Stored drop targets and key definitions remain independent when an +operation changes. + +Migration of older ALTER builders: + +* ``getPkColumns()`` and ``getUkColumns()`` now consistently return snapshots, + including manually built keys. Use their setters/adders or edit + ``getKeyConstraint().getColumns()`` to change SQL. The string setters replace + elements without retaining expression or ordering attributes. +* The legacy PK accessor remains a DROP-target alias when DROP, DROP UNIQUE or + DROP FOREIGN KEY is already selected. Replace builders that set PK columns + before selecting DROP with ``withDropColumns(...)``; changing the operation + does not transfer a key definition into deletion targets. +* ``getUkName`` accesses the UNIQUE definition's index name; the constraint + symbol remains ``getKeyConstraint().getName()``. KEY/INDEX flags read that same + definition. ``setUkTypeSpecified(false)`` removes the spelling; re-enabling it + without a spelling selects INDEX. +* Non-null key setters create a definition only when none exists. They throw + ``IllegalStateException`` for a competing constraint or index; use + ``setConstraint`` for an explicit replacement. Reads and null clears never + create a definition or restore values from a replaced one. + +Exclusion keys expose ``getExclusionOperatorReference()`` for structured access +to an operator's ``schemaName``, ``name`` and ``useOperatorKeyword`` flag. In +``Dialect.POSTGRESQL``, both CREATE and ALTER accept ``WITH OPERATOR(pg_catalog.&&)``. +Editing the reference updates both statement renderers. The existing string +``getExclusionOperator()`` returns its complete SQL spelling; its setter remains +available for replacing opaque operator text. New code can construct a reference +with ``new ExclusionOperator().withSchemaName("pg_catalog").withName("&&") +.withUseOperatorKeyword(true)``. + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE accounts ALTER COLUMN id TYPE bigint USING id + 1"); + AlterExpression.ColumnDataType column = alter.getAlterExpressions().get(0) + .getColDataTypeList().get(0); + Expression conversion = column.getUsingExpression(); + column.setUsingExpression(CCJSqlParserUtil.parseExpression("id * 10")); + String updatedSql = alter.toString(); + +Identity alterations are directly accessible through ``AlterExpression.getColumnIdentityActions()``; +the existing ``ColumnDataType.getIdentityAlterations()`` API remains available. +Sequence ownership is shared by ``CreateSequence`` and ``AlterSequence`` through ``Sequence.getOwnership()``: ``null`` means omitted, ``isNone()`` means explicit ``OWNED BY NONE``, and ``getColumn()`` identifies an owner. ``TablesNamesFinder`` includes ``LIKE`` sources and sequence owners without treating sequence or type names as tables. See `ALTER TABLE `_ and `ALTER SEQUENCE `_. + +For identity columns, ``RESTART 20`` and ``RESTART WITH 20`` produce the same +``IdentityAlteration`` with kind ``RESTART`` and ``getRestartWith() == 20L``. +The renderer consistently uses ``RESTART WITH 20``. A null restart value means +bare ``RESTART``, which uses the sequence's configured start value. Call +``setRestartWith`` to edit a parsed action or construct one with +``new IdentityAlteration(Kind.RESTART).withRestartWith(20L)``. + + +Edit column identity actions +---------------------------- + +``getColumnIdentityActions()`` returns active ``ALTER COLUMN`` identity actions +as ``ColumnIdentityAction`` entries. Each entry exposes the editable column name +and its ordered ``IdentityAlteration`` list, without requiring a data-type cast: + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE accounts ALTER COLUMN id " + + "ADD GENERATED BY DEFAULT AS IDENTITY (CACHE 10)", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + ColumnIdentityAction column = alter.getAlterExpressions().get(0) + .getColumnIdentityActions().get(0); + column.setColumnName("account_id"); + IdentityAlteration identity = column.getIdentityAlterations().get(0); + identity.setGenerationMode(IdentityDefinition.GenerationMode.ALWAYS); + identity.getParameters().get(0).setValue(20L); + System.out.println(alter); + // ALTER TABLE accounts ALTER COLUMN account_id + // ADD GENERATED ALWAYS AS IDENTITY (CACHE 20) + +The outer list has unmodifiable membership; its entries are the original AST +nodes. Request a new list after changing action membership or switching the +outer ALTER operation. The inner alteration list is mutable: replace an entry +to change its kind, or use ``setIdentityAlterations`` to replace the ordered +actions for a column. Unrelated actions and inactive inherited payloads are +excluded. An inline identity declaration in ``ADD COLUMN`` remains a +``ColumnOption`` rather than an identity alteration. + +``getGenerationMode``/``setGenerationMode`` work for both ``ADD`` and +``SET_GENERATED``. ``getParameters``/``setParameters`` work for ``ADD`` and +``SET_PARAMETER``. ADD access delegates to its existing ``IdentityDefinition``; +replacing or clearing that definition immediately changes both views. Parameters +are ordered mutable ``Sequence.Parameter`` values, with null meaning omitted. +RESTART and DROP retain their distinct ``getRestartWith`` and ``isIfExists`` +properties. A sequence such as ``SET CACHE 10 RESTART WITH 20`` remains two +ordered alterations. + +For a newly constructed ADD action without a definition, mode and parameter +getters return null. Non-null setters create the definition; setting parameters +first uses BY DEFAULT. Null setters do not create one. This does not change the +SQL grammar or validate whether a column already has an identity in a database. + + +Column defaults on tables and views +----------------------------------- + +``AlterExpression.getColumnDefaults()`` exposes active ``SET DEFAULT`` actions +through ``ColumnDefaultAction`` for both tables and views. The returned list is +read-only, but its entries are the original mutable AST objects: + +.. code-block:: java + + for (String objectType : List.of("TABLE", "VIEW")) { + Statement statement = CCJSqlParserUtil.parse( + "ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 1", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + List actions = statement instanceof Alter + ? ((Alter) statement).getAlterExpressions() + : ((AlterRelation) statement).getActions(); + for (AlterExpression action : actions) { + for (ColumnDefaultAction column : action.getColumnDefaults()) { + column.setDefaultExpression(new LongValue(42)); + } + } + System.out.println(statement); // SET DEFAULT 42 for either object type + } + +``getColumnName()`` and ``setColumnName()`` access the affected column. +``getDefaultExpression()`` exposes parsed expressions; ``getDefaultValue()`` +returns their SQL text. Existing ``ColumnSetDefault`` constructors still accept +opaque strings, for which ``getDefaultExpression()`` returns ``null``. +Replacing an expression also replaces any opaque value. Unrelated actions +return an empty list. This lookup follows the action's current rendering path: +changing an operation or retaining unused metadata does not make a fixed-body +action expose ``SET DEFAULT`` entries it will not render. + +``hasActiveColumnDefinitions()`` reports whether the action currently renders +its column definitions through the general ALTER body. ``getColDataTypeList()`` +continues to expose stored metadata even when another body takes precedence. +``AlterDeParser`` uses the active path, so expression visitors rewrite the +selected default or column expressions without rendering inactive metadata. +``TableDefinitionTraversal`` and the table-name finder visit the expressions +and tables belonging to the selected body. The metadata validator uses that +same selection. For example, adding a column definition to an action that still +stores an earlier foreign key renders the column and does not require the +inactive foreign table or its columns to exist. ``getConstraint()`` and +``getIndex()`` still expose the stored objects for direct inspection. + +``getActiveTableDefinition()`` returns the original definition contributing to +the current body, or ``null`` when another payload takes precedence. A legacy +``ADD CONSTRAINT`` header contributes only its keys; its unused storage options +are not traversed. Column options likewise visit only the payload selected by +their ``Kind``, including after changing an option to ``FOREIGN_OPTIONS``. + +Custom ``AlterExpression`` subclasses overriding ``appendBody`` should also +reflect their dispatch in ``usesColumnActionBody()``, ``usesGeneralBody()`` and +``hasActiveConstraintHeader()``. Subclasses rendering a separate definition can +override ``getActiveTableDefinition()``. The general-body hook also determines +``hasActiveColumnDefinitions()``. + +Structured column attributes +============================ + +``ColumnDefinition.getColumnOptions()`` exposes column nullability, ``COLLATE``, +``COMMENT``, ``ON UPDATE``, ``AUTO_INCREMENT``, visibility, inline ``PRIMARY KEY`` +and parenthesized generated expressions alongside existing defaults, references +and identity declarations. CREATE and ALTER column definitions use the same model. + +``ColumnOption.Kind`` selects the relevant accessor: ``getNullable()``, +``getCollation()``, ``getComment()``, ``getOnUpdateExpression()``, +``getVisible()``, ``getConstraint()`` or ``getGeneratedDefinition()``. +The AUTO_INCREMENT kind has no additional payload. A missing option does not +imply a server default. Option order and unknown raw extensions are preserved. + +``GeneratedColumnDefinition`` contains an ``Expression``, an explicit +``GENERATED ALWAYS`` flag, and nullable ``Storage`` (STORED/VIRTUAL). Identity +columns continue to use ``IdentityDefinition``. Visitors, validators and custom +expression deparsers traverse generation and ON UPDATE expressions and comment +literals. This is syntax modeling; server restrictions on permissible generation +expressions are not evaluated. + +Charset remains in ``ColDataType.getCharacterSet()``; column collation is in its +COLLATE option. Consumers can map both directly without reconstructing tokens. +StringValue comment bodies retain their SQL escape representation. + +A column DEFAULT value ends before a COLLATE at its top level, which becomes the +column's COLLATE option: MySQL's DEFAULT literal and PostgreSQL's DEFAULT ``b_expr`` +do not include COLLATE. A COLLATE inside parentheses, function arguments or a CASE +expression stays in the default expression. ``SET DEFAULT`` actions and CREATE +DOMAIN keep an expression COLLATE. + +Migration: ``DEFAULT 'N' COLLATE utf8mb4_bin`` in a column definition previously +produced one DEFAULT option holding a ``CollateExpression``. It now produces a +DEFAULT option with ``'N'`` and a COLLATE option with ``getCollation()``; +``getColumnSpecs()`` returns ``[DEFAULT, 'N', COLLATE, utf8mb4_bin]``. SQL output +is unchanged. + +MySQL column attributes have their own kinds: ``SRID`` (``getSrid()``), +``COLUMN_FORMAT`` (``getColumnFormat()``: FIXED, DYNAMIC or DEFAULT), and +``ENGINE_ATTRIBUTE``/``SECONDARY_ENGINE_ATTRIBUTE`` (``getEngineAttribute()`` +and ``isUseEquals()`` for the optional equals sign). Engine attribute literals +are visited and rendered like comment literals. Inline ``UNIQUE KEY`` is one +``KeyConstraint`` of type ``UNIQUE KEY``. A bare inline ``KEY``, MySQL's column +synonym for PRIMARY KEY, keeps its ``KEY`` spelling and reports +``ConstraintKind.PRIMARY_KEY``. Other values, such as Percona Server's +``COLUMN_FORMAT COMPRESSED``, remain opaque options. + +.. code-block:: java + + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT KEY, g POINT SRID 4326 COLUMN_FORMAT FIXED)"); + List columns = table.getColumnDefinitions(); + columns.get(0).getColumnOptions().get(0).getConstraintKind(); // PRIMARY_KEY + ColumnOption srid = columns.get(1).getColumnOptions().get(0); + srid.getSrid(); // 4326 + srid.setSrid(0); + columns.get(1).addColumnOptions( + ColumnOption.engineAttribute(new StringValue("{}"))); + // CREATE TABLE t (id INT KEY, g POINT SRID 0 COLUMN_FORMAT FIXED ENGINE_ATTRIBUTE '{}') + +Migration: these attributes were previously one opaque option per token, and +``UNIQUE KEY`` was a UNIQUE constraint followed by an opaque ``KEY``. Inspect the +kinds instead of the opaque nodes. ``getColumnSpecs()`` and ``getTokens()`` keep +the previous token split, for example ``[SRID, 4326]`` and ``[UNIQUE, KEY]``. +When constructing the KEY spelling, set the kind after the type: +``new KeyConstraint().withType("KEY").withKind(ConstraintKind.PRIMARY_KEY)``. +``CHAR(n) BINARY`` remains an opaque option: MySQL treats ``BINARY`` as part of +the data type's character set clause, not as a column attribute. + +``ColumnDefinition`` stores all options in one ordered list, including opaque +``ColumnOption.Kind.OTHER`` nodes. ``getColumnOptions()`` returns that mutable +list. ``getColumnSpecs()`` returns an **unmodifiable snapshot** for both raw and +structured options; expression fragments may occupy one token and structured +keywords use canonical capitalization. Unset options return null; an explicitly +empty list returns an empty snapshot. + +.. code-block:: java + + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT DEFAULT 1)"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + column.getColumnOptions().get(0).setDefaultExpression(new LongValue(2)); + column.addColumnSpecs("NOT", "NULL"); // keeps the structured DEFAULT node + // CREATE TABLE t (id INT DEFAULT 2 NOT NULL) + +Migration: calls such as ``getColumnSpecs().add(...)`` or ``.set(...)`` now throw +``UnsupportedOperationException`` instead of sometimes changing the AST and +sometimes editing a discarded copy. Use ``addColumnSpecs`` to append raw tokens, +edit the option nodes directly, or pass a modified token-list copy to +``setColumnSpecs`` to replace every option with opaque text. Raw tokens are never +automatically parsed. Consequently ``getColumnOptions()`` is also non-null for +raw specifications; inspect ``Kind`` to distinguish them. + +The constructor and ``setColumnSpecs`` copy supplied tokens. ``setColumnOptions`` +copies list membership but retains the mutable child nodes; later changes to the +caller's list do not change the column. Both getters return null after either +setter receives null. Appending preserves the current nodes and accepts an +immutable input list. An empty append preserves existing options and creates an +empty option list when none was set. + +``ColumnOption.getTokens()`` and ``GeneratedColumnDefinition.getTokens()`` also +return unmodifiable, detached snapshots. Reading tokens does not detach the +underlying expressions or other child nodes: their setters and direct edits +remain visible in SQL output, visitors and newly requested snapshots. + +For opaque ``Kind.OTHER`` options, copy the tokens, edit the copy, then call +``setTokens``. The setter and both ``ColumnOption.raw`` factories copy their +inputs; subsequent changes to the supplied list or array do not edit the AST. +Null raw tokens remain unset and return null; an empty list returns an empty +snapshot. Raw tokens are never parsed or classified. + +.. code-block:: java + + ColumnOption raw = ColumnOption.raw("SELECTIVITY", "10"); + List tokens = new ArrayList<>(raw.getTokens()); + tokens.set(1, "20"); + raw.setTokens(tokens); // SELECTIVITY 20 + +Migration: raw token getter edits now throw ``UnsupportedOperationException``, +as do edits to structured token snapshots that previously changed a discarded +copy. ``setTokens`` only accepts opaque options; it throws +``IllegalStateException`` for structured options, including SERIAL DEFAULT +VALUE. Use typed setters for structured edits. To explicitly replace a +structured option with raw text, replace its node in the column's option list: + +.. code-block:: java + + column.getColumnOptions().set(0, ColumnOption.raw("DEFAULT", "8")); + +MySQL column positions +~~~~~~~~~~~~~~~~~~~~~~ + +The trailing ``FIRST`` or ``AFTER column`` of an ALTER TABLE ADD, CHANGE or +MODIFY column definition is a ``ColumnPosition`` on the +``AlterExpression.ColumnDataType``, not a column option. ``getColumnSpecs()`` +still ends with its tokens, and ``setColumnSpecs`` replaces the options and the +position. Migration: ``getColumnOptions()`` no longer contains the two opaque +``AFTER`` and column-name nodes. As in MySQL, the position must follow the +column attributes; a position followed by further attributes is now rejected. + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD COLUMN c INT NOT NULL AFTER id"); + AlterExpression.ColumnDataType column = + alter.getAlterExpressions().get(0).getColDataTypeList().get(0); + column.getPosition().getColumnName(); // id + column.getColumnSpecs(); // [NOT, NULL, AFTER, id] + column.setPosition(ColumnPosition.first()); + // ALTER TABLE t ADD COLUMN c INT NOT NULL FIRST + +Inspect logical replication statements +====================================== + +PostgreSQL publications and subscriptions have separate statement and option models. No database connection is opened when these statements are parsed. + +.. code-block:: java + + CreatePublication publication = (CreatePublication) CCJSqlParserUtil.parse( + "CREATE PUBLICATION changes FOR TABLE accounts (id) WHERE (active = true)"); + PublicationTable target = publication.getTargets().get(0).getTables().get(0); + Table table = target.getTable(); + List columns = target.getColumns(); + Expression filter = target.getWhere(); + +A ``PublicationTarget`` distinguishes a group of explicit tables from ``TABLES IN SCHEMA``. Target order, repeated ``TABLE`` groups, ``ONLY`` and an explicit descendant ``*`` are preserved. ``CreatePublication.isAllTables()`` represents ``FOR ALL TABLES``; an empty target list without that flag means no target clause was specified. ``AlterPublication.getAction()`` distinguishes adding, replacing or removing targets from option, owner and name changes. + +``PublicationOption`` exposes typed operation sets, partition-root booleans and generated-column modes. ``SubscriptionOption`` has a separate key enum and typed streaming, origin, synchronous-commit and boolean accessors. The ordered option lists contain only explicitly written options; server defaults, which can differ across PostgreSQL versions, are not injected into the AST. A missing value on a boolean option represents its explicit short form, equivalent to ``= true``. + +.. code-block:: java + + CreateSubscription subscription = (CreateSubscription) CCJSqlParserUtil.parse( + "CREATE SUBSCRIPTION changes_sub CONNECTION 'dbname=app' " + + "PUBLICATION changes WITH (connect = false)"); + StringValue connection = subscription.getConnection(); + List publications = subscription.getPublications(); + Boolean connect = subscription.getOptions().get(0).getBooleanValue(); + +Connection strings remain string literals for lossless SQL regeneration. They can contain credentials and should not be logged without redaction. ``SubscriptionOption.isSlotNameNone()`` distinguishes the unquoted ``NONE`` keyword from a literal slot named ``'NONE'``. ``AlterSubscription`` covers connection changes, publication lists and refresh, enable/disable, options, skip LSN, ownership and renaming. + +Publication table columns and row filters participate in visitors and custom expression deparsers. ``TablesNamesFinder`` reports explicitly named publication tables, but cannot enumerate ``ALL TABLES`` or schema-wide targets without a catalog. Publication and subscription names are not table names. Feature classification reports schema modification; a subscription that may start asynchronous replication can additionally report possible data modification. + +See `CREATE PUBLICATION `_, `ALTER PUBLICATION `_, `CREATE SUBSCRIPTION `_ and `ALTER SUBSCRIPTION `_. + +Inspect type, domain and extension statements +============================================= + +PostgreSQL type DDL uses the existing column data-type model. A ``CreateType`` exposes its qualified name and a ``TypeDefinition``: ``EnumTypeDefinition``, ``CompositeTypeDefinition`` or ``RangeTypeDefinition``. A null definition denotes a shell type. Enum labels are ordered ``StringValue`` nodes, composite attributes carry a name, ``ColDataType`` and optional collation, and range options distinguish the subtype from names of support functions, collations and operator classes. + +.. code-block:: java + + CreateType type = (CreateType) CCJSqlParserUtil.parse( + "CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')"); + EnumTypeDefinition definition = (EnumTypeDefinition) type.getDefinition(); + String firstLabel = definition.getLabels().get(0).getValue(); + + AlterType alteration = (AlterType) CCJSqlParserUtil.parse( + "ALTER TYPE mood ADD VALUE IF NOT EXISTS 'fine' AFTER 'ok'"); + alteration.getPosition(); // AlterType.Position.AFTER + alteration.getNeighborValue(); // StringValue containing 'ok' + +``AlterType`` also models type ownership, renaming, schema moves and ordered composite-attribute changes. Base-type I/O definitions and base-type ``SET (...)`` alterations are not part of this support. + +``CreateDomain`` stores a ``ColDataType``, default expression and ordered ``DomainConstraint`` nodes. Each constraint distinguishes nullability from a check expression and preserves its optional name. ``AlterDomain`` models default/nullability changes, constraint actions, ownership, renaming and schema moves. Its ``isNotValid()`` flag describes the newly added check constraint. Domain expressions participate in statement visitors, expression deparsers and validation. + +``CreateExtension`` preserves ``IF NOT EXISTS``, the optional ``WITH``, and ordered schema/version/cascade options. Version identifiers and quoted versions are represented as ``Column`` and ``StringValue``, respectively. ``AlterExtension`` distinguishes update, schema change, and member addition/removal. An ``ExtensionObject`` identifies the object kind and target; function/procedure/aggregate members expose a ``RoutineReference`` with typed signature arguments, not call expressions. A null argument list means the signature was omitted, whereas an empty list means explicit ``()``. + +Type names, domain names and routine signatures are not reported as tables by ``TablesNamesFinder``; relation members of an extension are included. Extension scripts themselves are not inspected or executed. The obsolete ``CREATE EXTENSION ... FROM`` form is not supported. + +See PostgreSQL's documentation for `CREATE TYPE `_, `ALTER TYPE `_, `CREATE DOMAIN `_ and `ALTER EXTENSION `_. + +Classify a Statement ============================== -There are two features for handling errors +Every ``Statement`` can tell you **what it does** — whether it reads, writes, changes the schema, or sends rows back — without a second parse and without writing a visitor: -- ``parser.withErrorRecovery(true)`` will continue to the next statement separator and return an empty statement. -- ``parser.withUnsupportedStatements(true)`` will return an instance of the `UnsupportedStatement` class, although the first statement **must** be a regular statement +.. code-block:: java + + StatementFeatures features = CCJSqlParserUtil.parse(sqlStr).getFeatures(); + + if (features.returnsResultSet()) { + statement.executeQuery(sqlStr); + } else { + statement.executeUpdate(sqlStr); + } + +Why bother +------------------------------ + +Two jobs come up constantly, and both are traps if you approach them with string matching: + +**Safeguarding a read-only client.** Reporting tools, BI front-ends, LLM-generated SQL, user-supplied filters — plenty of code paths need to reject anything that writes, *before* the statement reaches the database. Checking whether the text starts with ``SELECT`` is not a safeguard. + +**Dispatching correctly.** JDBC wants ``executeQuery()`` for row-returning statements and ``executeUpdate()`` for the rest. Get it backwards and you get an exception, not a wrong answer, but you still have to decide. + +The reason a keyword check fails is that SQL is not organised into tidy Query/DML/DDL buckets. ``RETURNING`` turns a ``DELETE`` into a row source. A data-modifying CTE hides that ``DELETE`` inside something that begins with ``WITH``. An ``INSERT`` can contain a whole ``SELECT`` and still return nothing: + +.. list-table:: + :header-rows: 1 + :widths: 55 15 15 15 + + * - SQL + - returns rows + - reads + - writes + * - ``SELECT * FROM t`` + - yes + - yes + - no + * - ``INSERT INTO x SELECT * FROM t`` + - **no** + - yes + - yes + * - ``DELETE FROM t RETURNING *`` + - **yes** + - no + - yes + * - ``WITH c AS (DELETE FROM t RETURNING *) SELECT * FROM c`` + - yes + - no + - **yes** + * - ``INSERT INTO x WITH c AS (DELETE FROM t RETURNING *) SELECT * FROM c`` + - **no** + - no + - yes + * - ``CREATE TABLE t AS SELECT a FROM u`` + - no + - yes + - no (schema) + * - ``SELECT a INTO new_table FROM t`` + - **no** + - yes + - **yes** + +Note the last two rows of the fourth and fifth entries: ``RETURNING`` appearing *somewhere* in the statement is not the question. What matters is whether rows reach the client, and that is a property of the statement's own result position, not of any nested one. + +The features +------------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 25 40 35 + + * - ``StmtFeature`` + - Meaning + - Typical statements + * - ``READS_DATA`` + - reads persistent rows + - ``SELECT .. FROM t``, ``MERGE``, ``CREATE TABLE .. AS SELECT`` + * - ``RETURNS_RESULT_SET`` + - rows are sent back to the client + - ``SELECT``, ``DELETE .. RETURNING``, ``SHOW``, ``DESCRIBE``, ``EXPLAIN`` + * - ``MODIFIES_DATA`` + - rows are written or destroyed + - ``INSERT``, ``UPDATE``, ``DELETE``, ``MERGE``, ``UPSERT``, ``TRUNCATE``, ``DROP`` + * - ``MODIFIES_SCHEMA`` + - the catalogue changes + - ``CREATE``, ``ALTER``, ``DROP``, ``TRUNCATE``, ``GRANT``, ``COMMENT`` + * - ``MODIFIES_SESSION`` + - session state changes + - ``SET``, ``RESET``, ``USE``, ``DECLARE``, ``ALTER SESSION`` + * - ``MODIFIES_TRANSACTION`` + - transaction state or locks change + - ``COMMIT``, ``ROLLBACK``, ``SAVEPOINT``, ``LOCK``, ``SELECT .. FOR UPDATE`` + * - ``OPAQUE`` + - nothing further can be known statically + - ``CALL``, ``EXECUTE``, dynamic SQL, unsupported statements + +They are **not mutually exclusive**. ``INSERT .. RETURNING *`` carries ``MODIFIES_DATA`` *and* ``RETURNS_RESULT_SET``; ``TRUNCATE`` carries ``MODIFIES_SCHEMA`` *and* ``MODIFIES_DATA``, so that a guard looking only for data changes still stops it. + +Proven, possible, excluded +------------------------------ + +Each feature is three-valued, because some questions cannot be answered from syntax alone. ``SELECT nextval('s')`` writes; ``SELECT upper(name)`` does not; the parser cannot tell them apart, because volatility lives in the database catalogue, not in the SQL text. + +So a feature is either **proven**, **not excludable**, or **ruled out**, and you pick which side you want to be wrong on: .. code-block:: java - :caption: Error Recovery - CCJSqlParser parser = new CCJSqlParser( - "select * from mytable; select from; select * from mytable2" ); - Statements statements = parser.withErrorRecovery().Statements(); + StatementFeatures features = statement.getFeatures(); - // 3 statements, the failing one set to NULL - assertEquals(3, statements.size()); - assertNull(statements.get(1)); + features.is(StmtFeature.MODIFIES_DATA); // the grammar proves it + features.may(StmtFeature.MODIFIES_DATA); // proven, or could not be excluded - // errors are recorded - assertEquals(1, parser.getParseErrors().size()); +.. list-table:: + :header-rows: 1 + :widths: 25 20 55 + + * - Caller + - Uses + - Because + * - read-only guard + - ``may(..)`` + - a false negative lets a write through + * - JDBC dispatcher + - ``is(..)`` + - a false positive picks ``executeQuery`` for ``CREATE INDEX`` + +Convenience methods wrap the common combinations: .. code-block:: java - :caption: Unsupported Statement - Statements statements = CCJSqlParserUtil.parseStatements( - "select * from mytable; select from; select * from mytable2; select 4;" - , parser -> parser.withUnsupportedStatements() ); + features.returnsResultSet(); // is(RETURNS_RESULT_SET) + features.modifiesData(); // is(MODIFIES_DATA) + features.mayModifyData(); // may(MODIFIES_DATA) + features.modifiesSchema(); // is(MODIFIES_SCHEMA) + features.isOpaque(); // CALL, EXECUTE, dynamic SQL - // 4 statements with one Unsupported Statement holding the content - assertEquals(4, statements.size()); - assertInstanceOf(UnsupportedStatement.class, statements.get(1)); - assertEquals("select from", statements.get(1).toString()); +When the analysis identifies an unresolved function, procedure or dynamic SQL marker, it records the reference so you can consult your own catalogue or allow-list: - // no errors records, because a statement has been returned - assertEquals(0, parser.getParseErrors().size()); +.. code-block:: java + :caption: Safeguarding a read-only connection + + StatementFeatures features = CCJSqlParserUtil.parse(sqlStr).getFeatures(); + + if (connection.isReadOnly() && features.mayModifyData()) { + throw new SQLException( + "rejected, unresolved: " + features.getUnresolvedReferences()); + // e.g. [nextval] + } + +The generic fallback for an unclassified statement preserves ``OPAQUE`` and the possible effects ``READS_DATA``, ``RETURNS_RESULT_SET``, ``MODIFIES_DATA`` and ``MODIFIES_SCHEMA``, but does not add a Java statement or container class name to ``getUnresolvedReferences()``. The reference set can therefore be empty while the effects remain unknown. Check ``isOpaque()`` and ``may(..)``; an empty reference set alone does not establish safety. + +Existing explicit references and markers remain, including procedure and function names, ``do``, ``unsupported`` and ``explain``. Stored routine declarations also retain their existing statement-type markers, such as ``createfunction``; these are independent of the generic fallback. + +If you can prove some functions side-effect free, hand in a predicate and the uncertainty collapses: + +.. code-block:: java + + Set pure = Set.of("upper", "lower", "coalesce"); + + StatementFeatures features = statement.getFeatures(pure::contains); + + // SELECT upper(name) FROM t + features.mayModifyData(); // false + features.getUnresolvedReferences(); // empty + +.. warning:: + + The verdict is a **syntactic claim, not a semantic guarantee**. A user-defined function, a trigger on the target table or a ``CALL`` can do anything. Use this to reject obviously dangerous SQL early; it does not replace database-side permissions. + +Scripts +------------------------------ + +``Statements`` is an ``ArrayList`` and not a ``Statement``, so it has no ``getFeatures()`` of its own. Two entry points, for two different questions: + +.. code-block:: java + + Statements script = CCJSqlParserUtil.parseStatements( + "UPDATE t SET a = 1; SELECT a FROM t;"); + + // one union verdict — for guards + StatementFeatures all = StatementFeatureVisitor.analyse(script); + all.modifiesData(); // true + all.returnsResultSet(); // true + + // one verdict per statement, in order — for dispatchers + List each = StatementFeatureVisitor.analyseEach(script); + each.get(0).returnsResultSet(); // false, the UPDATE + each.get(1).returnsResultSet(); // true, the SELECT + +The union answers *"may this script write anything?"*. It cannot answer *"executeQuery or executeUpdate?"*, because it never says which statement returns the rows. + +.. note:: + + Nothing is cached. The tree is mutable and you may build statements by hand, so the verdict is recomputed on every call — microseconds against a millisecond-scale parse. + + +Find Table Names +============================== + +``net.sf.jsqlparser.util.TablesNamesFinder`` returns every table name in a statement or an expression, including the ones buried in sub-selects. + +.. code-block:: java + + // find in Statements + String sqlStr = "select * from A left join B on A.id=B.id and A.age = (select age from C)"; + Set tableNames = TablesNamesFinder.findTables(sqlStr); + assertThat( tableNames ).containsExactlyInAnyOrder("A", "B", "C"); + + // find in Expressions + String exprStr = "A.id=B.id and A.age = (select age from C)"; + tableNames = TablesNamesFinder.findTablesInExpression(exprStr); + assertThat( tableNames ).containsExactlyInAnyOrder("A", "B", "C"); + +A name is left out only where it resolves to a ``WITH`` item in scope. An alias hides no table, and without ``RECURSIVE`` a ``WITH`` item does not see itself or the items after it. The target of a DML statement is always reported. So the result may hold names that are no tables on some databases, such as a ``WITH`` item that recurses without ``RECURSIVE``: check it against the metadata of the database if that matters. Use the Visitor Patterns ============================== -Traverse the Java Object Tree using the Visitor Patterns: +Casting your way down the tree works for one known shape. For anything general — every column in a query, every table in a script — use a visitor: you override only the node types you care about and the adapters walk the rest. + +There is one visitor interface per layer of the model, and an ``..Adapter`` base class for each that already implements the full traversal: + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Adapter + - Reacts to + * - ``StatementVisitorAdapter`` + - statements: ``Select``, ``Insert``, ``CreateTable``, … + * - ``SelectVisitorAdapter`` + - query bodies: ``PlainSelect``, ``SetOperationList``, ``WithItem``, … + * - ``ExpressionVisitorAdapter`` + - expressions: ``Column``, ``Function``, ``EqualsTo``, … + * - ``FromItemVisitorAdapter`` + - FROM items: ``Table``, ``ParenthesedSelect``, ``TableFunction``, … .. code-block:: java @@ -225,28 +1402,15 @@ Traverse the Java Object Tree using the Visitor Patterns: // Invoke the Statement Visitor without a context stmt.accept(statementVisitor, null); -Find Table Names -============================== - -The class ``net.sf.jsqlparser.util.TablesNamesFinder`` can be used to return all Table Names from a Query or an Expression. - -.. code-block:: java - - // find in Statements - String sqlStr = "select * from A left join B on A.id=B.id and A.age = (select age from C)"; - Set tableNames = TablesNamesFinder.findTables(sqlStr); - assertThat( tableNames ).containsExactlyInAnyOrder("A", "B", "C"); +.. tip:: - // find in Expressions - String exprStr = "A.id=B.id and A.age = (select age from C)"; - tableNames = TablesNamesFinder.findTablesInExpression(exprStr); - assertThat( tableNames ).containsExactlyInAnyOrder("A", "B", "C"); + The second parameter of every ``visit()`` is a free-form **context** object of your choosing, threaded through the traversal. Pass ``null`` when you do not need it. Build a SQL Statement ============================== -Build any SQL Statement from Java Code using a fluent API: +The object model works in both directions. Build the tree from Java and print it as SQL: .. code-block:: java @@ -273,15 +1437,528 @@ Build any SQL Statement from Java Code using a fluent API: Assertions.assertEquals(expectedSQLStr, builder.toString()); +The same visitor can render an entire statement list: + +.. code-block:: java + + Statements statements = CCJSqlParserUtil.parseStatements("SELECT 1; SELECT 2;"); + StringBuilder script = new StringBuilder(); + statements.accept(new StatementDeParser(script), null); + Assertions.assertEquals("SELECT 1;\nSELECT 2;\n", script.toString()); + +Statement lists and nested blocks share the separator policy used by +``Statements.toString()``. Blocks and ``IF/ELSE`` statements retain their own +semicolon settings. Custom deparsers receive each child statement and the +``IF/ELSE`` condition through the visitor API, with the supplied context. + +Customize JSON expression output +-------------------------------- -Define the Parser Features +``ExpressionDeParser`` visits structured children of ``JsonFunction`` and +``JsonTableFunction`` while rendering them. A custom expression printer can +rewrite inputs, paths, PASSING values and DEFAULT expressions, including nested +JSON_TABLE columns, without changing the AST. JSON object keys and values are +visited when they are ``Expression`` objects; opaque objects and additional +query-path strings retain their existing rendering. + +.. code-block:: java + + Statement statement = CCJSqlParserUtil.parse( + "SELECT JSON_VALUE((SELECT doc FROM source_table), '$.a')"); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("payload"); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), + expressions.getBuilder()), null); + // SELECT JSON_VALUE((SELECT payload FROM source_table), '$.a') + +``StatementDeParser`` connects the select and expression printers, so subqueries +inside JSON expressions use the same printer. For expression-only use with +subqueries, connect an appropriate ``SelectDeParser`` explicitly. + +JSON nodes also expose ``appendTo(StringBuilder, Consumer)`` for +direct rendering. The consumer prints each child into the same builder; +``ExpressionDeParser`` forwards the visitor context through that callback. +``JsonFunction`` additionally has consumer overloads of ``appendObject``, +``appendArray``, ``appendValue``, ``appendQuery`` and ``appendExists``. + +Subclass migration: override these consumer overloads or ``appendTo`` to +customize both default and visitor output. The deparser enters through ``appendTo``; +overriding only a legacy single-argument ``append*`` or ``toString()`` method does +not customize that path. ``JsonFunction`` also uses the consumer overloads for +type-specific default rendering. Subclasses of concrete JSON_TABLE column +definitions should move ``toString()`` customization +to ``appendTo``. Direct subclasses of the abstract ``JsonTableColumnDefinition`` +retain their opaque ``toString()`` fallback until they override ``appendTo``. + + +ODBC timestamp intervals ============================== -JSQLParser interprets Squared Brackets ``[..]`` as Arrays, which does not work with MS SQL Server and T-SQL. Please use the Parser Features to instruct JSQLParser to read Squared Brackets as Quotes instead. +In ODBC escapes such as ``{fn TIMESTAMPADD(SQL_TSI_YEAR, 2, travel_date)}`` and +``{fn TIMESTAMPDIFF(SQL_TSI_DAY, start_date, end_date)}``, the first argument is a +``DateUnitExpression`` for the nine standard ``SQL_TSI_*`` interval keywords. +The original ODBC keyword is preserved on output and is not visited as a column. +This applies only to unqualified, escaped calls with three arguments and a bare +interval keyword. Ordinary calls, qualified names, quoted identifiers and other +arguments keep their existing expression interpretation. -JSQLParser allows for standard compliant Single Quote ``'..`` Escaping. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature. +Handle Parse Errors +============================== + +``CCJSqlParserUtil.parse(String, ...)`` requires a statement: null and empty string +inputs throw ``JSQLParserException``, matching the default behavior for whitespace-only +and comment-only input. ``CCJSqlParserUtil.parseStatements(String, ...)`` returns a new, +mutable empty ``Statements`` list for null or empty input, as it already does for +whitespace-only and comment-only input. This applies to the overloads with parser +configuration callbacks and caller-provided executors; caller-provided executors remain +open. These empty-input results replace the previous null returns of these methods. + +By default a syntax error aborts the whole parse. Two features let a script survive one bad statement: -Additionally there are Features to control the Parser's effort at the cost of the performance. +- ``parser.withErrorRecovery(true)`` skips to the next statement separator and returns an empty statement. +- ``parser.withUnsupportedStatements(true)`` captures an unrecognised statement root as + ``UnsupportedStatement``; it may be the first statement. Failures in recognised statement + grammar still throw, or use ``withErrorRecovery(true)`` and record the error. Thus + ``SELECT * FROM``, ``INSERT INTO t (a`` and ``UPDATE t SET`` do not become opaque statements. + The two options serve different purposes; enabling unsupported capture does not suppress + recognised syntax errors. + +.. code-block:: java + :caption: Error Recovery + + CCJSqlParser parser = new CCJSqlParser( + "select * from mytable; select from; select * from mytable2" ); + Statements statements = parser.withErrorRecovery().Statements(); + + // 3 statements, the failing one set to NULL + assertEquals(3, statements.size()); + assertNull(statements.get(1)); + + // errors are recorded + assertEquals(1, parser.getParseErrors().size()); + +.. code-block:: java + :caption: Unsupported Statement + + Statements statements = CCJSqlParserUtil.parseStatements( + "select * from mytable; shutdown defrag; select * from mytable2; select 4;" + , parser -> parser.withUnsupportedStatements() ); + + // 4 statements with one Unsupported Statement holding the content + assertEquals(4, statements.size()); + assertInstanceOf(UnsupportedStatement.class, statements.get(1)); + assertEquals("shutdown defrag", statements.get(1).toString()); + + // no errors records, because a statement has been returned + assertEquals(0, parser.getParseErrors().size()); + +.. note:: + + Opaque capture is not SQL validation. An unrecognised root or an existing opaque + grammar branch cannot establish validity in an unknown dialect. Callers that need + validity must validate against that dialect. Recognised syntax failures formerly + captured by the generic fallback now throw or produce an error-recovery placeholder; + partial IF results and typed prefixes with invalid trailing clauses are not published + as successful statements. ``SingleStatement()`` remains an incremental entry point; + use ``Statement()`` or ``Statements()`` to check complete statement boundaries. + + An ``UnsupportedStatement`` is reported as ``OPAQUE`` by :ref:`Classify a Statement` — nothing about its effects is knowable. + + +Choose a Dialect +============================== + +One grammar covers every supported RDBMS, but a few pieces of syntax mean different things in different products. Those are switched with parser features, and a ``Dialect`` preset turns on the right set for you. + +.. code-block:: java + + // MySQL: backslash escapes, hash line comments, double-quoted strings + Statement stmt = CCJSqlParserUtil.parse( + "SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42#24" + , parser -> parser.withDialect(Dialect.MYSQL) ); + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - ``Dialect`` + - Turns on + * - ``MYSQL`` + - ``withBackslashEscapeCharacter``, ``withHashLineComments``, ``withDoubleQuotedStrings`` (MySQL and MariaDB, the last for the default ``sql_mode``) + * - ``SQLSERVER`` + - ``withSquareBracketQuotation`` and ``CLUSTERED`` / ``NONCLUSTERED`` options on table-level primary key and unique constraints and ``CREATE INDEX`` + * - ``POSTGRESQL`` + - tagged dollar strings, the newline rule for ordinary string literals, literal-local ``E'...'`` escapes, and preservation of dots inside quoted names + * - ``ANSI_SQL`` + - the newline rule for adjacent string literals + * - ``BIGQUERY`` + - ``withDoubleQuotedStrings``, ``withBackslashEscapeCharacter``, ``withHashLineComments``, any-whitespace rule for adjacent string literals + * - ``DATABRICKS`` + - ``withDoubleQuotedStrings``, ``withBackslashEscapeCharacter``, any-whitespace rule for adjacent string literals + * - ``SNOWFLAKE`` + - ``withBackslashEscapeCharacter`` only, double quotes stay quoted identifiers + * - ``INFORMIX`` + - Informix ``ALTER TABLE ... ADD CONSTRAINT`` definitions with optional trailing constraint names + * - ``SPANNER`` + - GoogleSQL ``CREATE [UNIQUE] NULL_FILTERED INDEX`` with a separate null-filtering flag + * - ``DORIS`` + - ``JOIN [shuffle]`` and ``JOIN [broadcast]`` distribution hints + * - ``COCKROACHDB`` + - ``ALTER TABLE ... ALTER PRIMARY KEY USING COLUMNS (...)`` with optional hash sharding and storage parameters + * - ``TERADATA`` + - ``UPDATE target FROM sources SET ...`` with the FROM clause before SET + * - ``SQLITE`` + - ``INSERT OR ABORT``, ``INSERT OR FAIL``, ``INSERT OR IGNORE`` and ``INSERT OR ROLLBACK`` conflict clauses + +Features set explicitly *after* the preset win over it. + +MySQL comments +~~~~~~~~~~~~~~ + +With ``Dialect.MYSQL``, ``--`` starts a line comment only when followed by +whitespace or a control character. Thus ``SELECT 1--2`` contains subtraction +and a negative operand. SQL inside an unversioned executable comment is parsed +normally: ``SELECT 1 /*! + 2 */`` becomes ``SELECT 1 + 2``. Optimizer hints +(``/*+ ... */``) remain separate from executable comments. + +Conditional executable comments require an explicit target server version: + +.. code-block:: java + + Statement stmt = CCJSqlParserUtil.parse( + "SELECT 1 /*!90702 + 2 */", + parser -> parser.withDialect(Dialect.MYSQL).withMySqlServerVersion(90702)); + +The value is ``major * 10000 + minor * 100 + patch``; for example, ``90702`` +means 9.7.2 and ``260700`` means 26.7.0. Five-digit and six-digit comment version +prefixes follow MySQL's whitespace rules. SQL for a newer server is ignored; +SQL for the configured version or an older one participates in the AST. +Both SQL renderers produce the resolved SQL for that target, without the +conditional wrappers. Without a target, a conditional executable comment +raises an error instead of silently discarding its SQL. This replaces the +previous treatment of all executable comments as ordinary discarded comments. +The same configuration applies to parsers constructed with a ``StreamProvider`` +or ``FeatureConfiguration``. Other dialects retain their comment behavior. +Nested executable comments are rejected; nested-comment acceptance in other +dialects is not a guarantee that the same SQL is valid on MySQL. + +ALTER column names +~~~~~~~~~~~~~~~~~~ + +Non-reserved names such as ``comment`` work unquoted in ``ADD``, ``MODIFY``, +``CHANGE``, ``DROP`` and ``RENAME`` column actions. The ``COLUMN`` keyword does +not change how the name is interpreted. Column definitions remain editable: + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t MODIFY COLUMN comment TEXT", + parser -> parser.withDialect(Dialect.MYSQL)); + AlterExpression.ColumnDataType column = alter.getAlterExpressions().get(0) + .getColDataTypeList().get(0); + column.setColumnName("notes"); + column.getColDataType().setDataType("LONGTEXT"); + // ALTER TABLE t MODIFY COLUMN notes LONGTEXT + String sql = alter.toString(); + +PostgreSQL names and literals +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Select ``Dialect.POSTGRESQL`` when parsing PostgreSQL SQL. For example, +``DROP INDEX "a.b"`` has the single name ``"a.b"`` and no schema; the dot +inside the quotes is not a separator. This applies to table and column +references throughout DDL and DML. The default configuration retains its +historical name-splitting behavior for compatibility with BigQuery names. + +``COMMENT ON TABLE t IS $tag$body$tag$`` accepts tagged dollar strings with +the PostgreSQL preset, or with ``withDollarQuotedStringTags(true)``. Tagged +strings remain disabled by default. Table, column and view comments preserve +their dollar delimiter and literal body. Ordinary single-quoted strings +separated by a newline concatenate; dollar-quoted strings do not. +``E'...'`` enables backslash escapes for that literal without changing +the treatment of ordinary strings elsewhere in the statement. + +The shared type grammar accepts negative scales, including +``numeric(2, -3)``, in type fragments, DDL and casts. ``getPrecision()`` +returns ``2`` and ``getScale()`` returns ``-3``; an omitted scale returns +``null``. PostgreSQL's precision and scale range checks remain the database's +responsibility. When constructing a ``ColDataType`` directly, +``ColDataType.fromNumericParameters("numeric", 2, -3)`` accepts negative scales; +use ``null`` for omitted parameters. The legacy ``int`` +constructor continues to treat negative arguments as omitted parameters. + +String literal values +~~~~~~~~~~~~~~~~~~~~~ + +``StringValue.getValue()`` returns the literal body as written, and ``toString()`` +renders it unchanged. ``getNotExcapedValue()`` returns the characters it denotes. +A doubled quote of the literal's own quote character is resolved, so +``"it""s"`` yields ``it"s``. ``getEscaping()`` records how the parser read +backslashes: + +* ``MYSQL_BACKSLASH`` without a dialect or with ``Dialect.MYSQL``/``MARIADB`` when + backslash escapes are enabled: ``'it\'s'`` yields ``it's``, ``\n``, ``\t``, + ``\0`` and ``\Z`` become their characters, and ``\%`` and ``\_`` keep their backslash. +* ``POSTGRESQL_ESCAPE_STRING`` for ``E'...'`` with ``Dialect.POSTGRESQL``, including + ``\x``, octal and ``\u`` escapes; octal and hexadecimal escapes are UTF-8 bytes. +* ``POSTGRESQL_UNICODE_ESCAPE`` for ``U&'...'``, see + `PostgreSQL Unicode escape strings`_. +* ``STANDARD`` otherwise, including dollar-quoted strings and constructed values; + backslashes stay literal. Other dialects with backslash escapes keep this reading. + +``setEscaping()`` selects the reading for a constructed value. Escaping is not +part of ``equals()``, like the quote character. + +PostgreSQL Unicode escape strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With ``Dialect.POSTGRESQL``, ``U&'...'`` and an optional ``UESCAPE 'c'`` clause form one +``StringValue``. Its prefix is ``U&``, ``getValue()`` returns the body as written, and +``toString()`` renders the literal with its clause. ``getUnicodeEscapeCharacter()`` returns +the ``UESCAPE`` character, or ``null`` without the clause, when the escape character is a +backslash. ``getNotExcapedValue()`` resolves the escapes as PostgreSQL does: + +* the escape character followed by four hexadecimal digits, or by ``+`` and six, is that + code point. A character above U+FFFF can also be written as a UTF-16 surrogate pair of + two escapes; +* a doubled escape character and a doubled quote stand for themselves. + +.. code-block:: java + + Comment comment = (Comment) CCJSqlParserUtil.parse( + "COMMENT ON TABLE t IS U&'d!0061t!+000061' UESCAPE '!'", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + comment.getComment().getNotExcapedValue(); // "data" + comment.toString(); // COMMENT ON TABLE t IS U&'d!0061t!+000061' UESCAPE '!' + +* The parser rejects what PostgreSQL rejects: + an escape without its digits, code point 0 or one above U+10FFFF, an unpaired surrogate, + and an escape character that is a hexadecimal digit, ``+``, a quote, whitespace or not ASCII. + ``getNotExcapedValue()`` throws ``IllegalArgumentException`` for such a constructed value. +* Without ``Dialect.POSTGRESQL``, ``U&'x'`` keeps its previous reading: the column ``U``, + the ``&`` operator and the string ``'x'``. +* ``UESCAPE`` may follow the literal after spaces, tabs or newlines, but not after a comment + or a continuation line. +* ``U&"..."`` quoted identifiers are not supported. +* The ``UESCAPE`` character is not part of ``equals()``, like the escaping. + +Other dialect-specific syntax +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +MySQL user-variable targets in ``SELECT ... INTO @variable`` require +``Dialect.MYSQL`` or ``Dialect.MARIADB``. They are stored in +``PlainSelect.getMySqlSelectIntoClause().getVariables()`` as ``UserVariable`` +expressions, with the clause position preserved before ``FROM`` or at the end +of the query. They are not table targets in ``getIntoTables()``. + +Doris distribution hints require ``parser.withDialect(Dialect.DORIS)``. +``Join.getJoinHint()`` exposes the keyword and ``Position.AFTER_JOIN``; +the existing SQL Server hints use ``Position.BEFORE_JOIN``. Rendering preserves +both the position and the brackets around a Doris hint. + +CockroachDB primary-key changes require ``parser.withDialect(Dialect.COCKROACHDB)``. +Their action is an ``AlterExpressionPrimaryKey`` with key elements and storage +parameters in ``getConstraint()``. ``isUsingHash()`` preserves ``USING HASH``, while +``getBucketCount()`` holds the legacy ``WITH BUCKET_COUNT = expression`` value. +The newer ``WITH (bucket_count = expression)`` form uses the index storage parameters. + +With ``Dialect.TERADATA``, ``UPDATE a FROM target a, source b SET a.id = b.id`` +uses the existing ``Update`` model's ``fromItem`` and ``joins`` properties. +``isFromBeforeSet()`` preserves the clause position in both SQL renderers. +Table discovery and metadata validation recognize a target alias declared in +that FROM clause. Other dialects retain the existing FROM-after-SET syntax. + +``Dialect.SQLSERVER`` supports methods on expression results, including +``(SELECT ... FOR XML PATH(''), TYPE).value('.', 'varchar(max)')``. +``MethodCallExpression`` exposes the receiver expression and a ``Function`` +containing the method name and arguments. Field access and method calls share +the navigation grammar; expression visitors and deparsers traverse both the +receiver and method arguments. XQuery strings remain string literals. + +``Dialect.POSTGRESQL`` enables ``DO [LANGUAGE name] code [LANGUAGE name]``, +with the language clause allowed once, before or after the body. +``DoStatement.getCode()`` is a ``StringValue`` that preserves the literal's +quotes, dollar tag and body text. The optional language and its position have +separate properties; an omitted language remains unspecified in the AST. +The body is language-specific source, not a parsed PL/pgSQL statement tree. +Expression visitors can inspect or replace the body literal. Feature analysis +reports ``OPAQUE``; table discovery rejects this statement because the body's +table accesses are unknown. Validation checks the ``doStatement`` capability, +without validating the procedural language inside the literal. + +Enable the PostgreSQL dialect when parsing a script containing a ``DO`` block: + +.. code-block:: java + + Statements statements = CCJSqlParserUtil.parseStatements( + "DO $$BEGIN RAISE NOTICE 'hello'; END$$; SELECT 1;", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + DoStatement block = (DoStatement) statements.get(0); + String body = block.getCode().getValue(); + // body: BEGIN RAISE NOTICE 'hello'; END + // statements.get(1) is the following SELECT. + +Semicolons and SQL statements inside the body remain part of its string literal; +they do not split the surrounding script into additional statements. + +PostgreSQL ``CREATE FUNCTION`` and ``CREATE PROCEDURE`` declarations also preserve +``AS '...'``, ``AS E'...'`` and dollar-quoted bodies as opaque text. Their +``getFunctionDeclarationParts()`` list retains the body and trailing options, +while a following statement is parsed separately. Newline-separated body string +continuations retain the newline needed when the declaration is rendered again. +Use ``Dialect.POSTGRESQL`` for tagged dollar quotes and PostgreSQL-specific DDL +such as schema-qualified index collations and operator classes. This support +does not validate PL/pgSQL source or build an AST for statements inside the body. + +With ``Dialect.POSTGRESQL``, ``#`` terminates an unquoted identifier, so JSON +operators such as ``js#>>'{a}'`` and ``js#>'{a}'`` work without surrounding +spaces. Quote identifiers containing ``#``, for example ``"js#"``. Other +dialects retain their existing identifier and hash-comment rules. + +With ``Dialect.SQLSERVER``, ``SET NOCOUNT ON`` and grouped boolean options such as +``SET QUOTED_IDENTIFIER, ANSI_NULLS OFF`` use ``SetStatement.getOnOffOptions()``. +The ordered ``OnOffOption`` list and shared ``isOn()`` value are editable; +``setOnOffOptions()`` replaces generic assignments and their scope. Both SQL +renderers share statement punctuation while generic assignments retain expression +visitor support. Parsing a SET directive records it without changing lexer settings. +``Dialect.SQLSERVER`` enables ``INSERT BULK table (name type, ...) WITH (...)``. +``InsertBulk`` exposes the target table, existing ``ColumnDefinition`` models, +and ordered typed options, including ``ROWS_PER_BATCH`` and ``ORDER`` keys. +The SQL declaration is preserved; the following binary bulk-load data stream +is outside the SQL parser. Visitors and deparsers traverse option values and +ordering expressions. Validation uses the ``insertBulk`` capability. + +With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and +``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()`` +for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words +retain their existing interpretation as optional index names. +SQL Server ``CREATE TABLE`` also accepts a trailing comma after the final column +or table constraint. SQL output normalizes the definition by omitting that comma. + +``CREATE UNIQUE NONCLUSTERED INDEX ix ON t (id)`` also requires +``Dialect.SQLSERVER``. Uniqueness remains in ``Index.getType()`` and clustering +is stored separately in ``Index.getClustering()``. With ``Dialect.SPANNER``, +``CREATE UNIQUE NULL_FILTERED INDEX ix ON t (id)`` stores null filtering in +``CreateIndex.isNullFiltered()``. An omitted clustering or null-filtering option +is not supplied from database defaults. The Spanner preset currently selects +this index syntax; it does not configure GoogleSQL string-literal rules. + +With ``Dialect.POSTGRESQL``, index keys accept schema-qualified collation and +operator-class names, for example ``name COLLATE pg_catalog."C" +pg_catalog.text_ops ASC NULLS LAST``. Function keys such as ``lower(name)`` are +stored as expressions. Key attributes are available through ``getCollation()``, +``getOperatorClass()``, ``getOperatorClassParameters()``, ``getSortOrder()`` and +``getNullOrdering()`` on ``KeyElement``. Under this dialect these +attributes are not duplicated in the legacy ``getParams()`` list, so changing +or removing them is reflected when rendering SQL. Other dialects retain the +legacy parameter representation, including MySQL prefix lengths. + +``CreateIndexDeParser`` and ``StatementDeParser`` pass key expressions, +structured option values and the partial-index predicate to their expression +visitor. ``StatementVisitorAdapter``, ``TablesNamesFinder`` and index validation +traverse the same structured expressions. + +Informix's constraint form requires an explicit dialect selection: + +.. code-block:: java + + Statement stmt = CCJSqlParserUtil.parse( + "ALTER TABLE child ADD CONSTRAINT FOREIGN KEY (id) " + + "REFERENCES parent(id) CONSTRAINT fk_child", + parser -> parser.withDialect(Dialect.INFORMIX)); + +Row pattern matching (MATCH_RECOGNIZE) +-------------------------------------- + +``MatchRecognize`` is a ``FromItem`` wrapping the input relation. Its input +alias and output alias are independent. ``getMeasures()`` and +``getDefinitions()`` expose ordinary SQL expressions; ``getPattern()`` exposes +an editable ``RowPattern`` tree with variables, groups, ordered alternatives, +sequences, anchors and quantifiers. Oracle and Snowflake additionally support +``PERMUTE`` and exclusion nodes. Permutations remain compact in the AST. + +.. code-block:: java + + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT * FROM events MATCH_RECOGNIZE (" + + "ORDER BY seq MEASURES SUM(A.price) AS total " + + "PATTERN (A+) DEFINE A AS price > 0 " + + "OPTIONS (use_longest_match = TRUE))", + parser -> parser.withDialect(Dialect.BIGQUERY)); + MatchRecognize match = (MatchRecognize) select.getFromItem(); + RowPattern.Quantified repetition = (RowPattern.Quantified) match.getPattern(); + repetition.setReluctant(true); + System.out.println(select); // PATTERN (A+?) + +``FromItemVisitorAdapter`` traverses the input and clause expressions. +``RowPatternVisitorAdapter`` traverses pattern nodes and optionally visits +expressions in quantifier bounds. ``RowPatternFunction`` represents an explicit +``RUNNING`` or ``FINAL`` prefix while exposing the underlying ``Function`` to +existing expression visitors. Both ``toString()`` and the deparsers render the +current AST, including edited nested ``STRUCT`` arguments. + +Select the source dialect explicitly: + +* ``BIGQUERY`` requires ``ORDER BY``, ``MEASURES``, ``PATTERN`` and ``DEFINE``. + It supports empty alternatives, literal or query-parameter bounds, adjacent + anchors such as ``^A+$``, and ``OPTIONS (use_longest_match = TRUE/FALSE)``. +* ``ORACLE`` supports optional ordering/measures, row output modes, targeted + ``AFTER MATCH SKIP``, ``SUBSET``, permutations, exclusions and function modes. +* ``SNOWFLAKE`` supports the corresponding documented forms except ``SUBSET`` + and BigQuery options. Its documented alternation-before-concatenation + precedence is used for the pattern AST. Other presets use + concatenation-before-alternation. Rendering adds explicit parentheses at + mixed operator boundaries; it does not translate expression syntax between DBs. + +Validation checks the ``matchRecognize`` and ``matchRecognizeOptions`` features. +With an explicit dialect, it also checks selected static rules: pattern variable +names, bounds, subset/skip targets and incompatible clause options. Table +finding does not treat pattern qualifiers such as ``A.price`` as table names. +Metadata validation does not bind these columns to the input relation. +Function eligibility, aggregation/type rules, data-dependent skip failures and +query execution remain the database's responsibility. + +The test fixtures were compared before and after deparsing using GoogleSQL's +reference evaluator and Oracle 26ai Free. The reference evaluator is not the +BigQuery service. Snowflake coverage follows its documentation and parser +round-trip tests; it has not been executed against a Snowflake account. + +See the official `BigQuery MATCH_RECOGNIZE syntax +`_, +`Oracle row pattern matching guide +`_ +and `Snowflake MATCH_RECOGNIZE reference +`_. + +The individual features +------------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Feature + - What it changes + * - ``withSquareBracketQuotation`` + - ``[..]`` reads as a quoted identifier instead of an array — needed for T-SQL on MS SQL Server and Sybase + * - ``withBackslashEscapeCharacter`` + - ``\\..`` escaping inside string literals, in addition to the standard ``'..`` doubling + * - ``withDoubleQuotedStrings`` + - ``".."`` reads as a string literal instead of a quoted identifier (BigQuery, Spark/Databricks, MySQL default ``sql_mode``); combined with ``withBackslashEscapeCharacter``, ``\\"`` does not end the literal, as in MySQL ``"it\\"s"`` + * - ``withHashLineComments`` + - ``#`` starts a line comment + * - ``withBlankLinesAsSeparator`` + - ``false`` stops two empty lines (``\n\n\n``) ending a statement, so they read as plain whitespace + * - ``withAdjacentStringLiterals`` + - adjacent string literals concatenate: ``NEWLINE`` (SQL standard, PostgreSQL) or ``WHITESPACE`` (GoogleSQL, Spark/Databricks); ``true`` selects ``NEWLINE``, ``false`` switches it off + * - ``withAllowComplexParsing`` + - permits deeply nested expressions, at a significant performance cost + * - ``withTimeOut`` + - aborts parsing after N milliseconds .. code-block:: java @@ -317,4 +1994,794 @@ Additionally there are Features to control the Parser's effort at the cost of th sqlStr , parser -> parser .withBackslashEscapeCharacter(true) - ); \ No newline at end of file + ); + +Things that trip people up +------------------------------ + +.. hint:: + + 1) **Quoting:** Double quotes ``".."`` quote identifiers. Square brackets ``[..]`` are arrays unless you turn on ``withSquareBracketQuotation``. + + 2) **Reserved keywords:** JSQLParser uses a more restrictive list than most databases, and such keywords **need to be quoted**. + + 3) **Escaping:** standard single-quote ``'..`` escaping is always on. Backslash escaping is not — set ``withBackslashEscapeCharacter``. + + 4) **Oracle alternative quoting** is partially supported, for common brackets: ``q'{...}'``, ``q'[...]'``, ``q'(...)'`` and ``q''...''``. + + +Compile from Source Code +============================== + +You need ``JDK 8`` or ``JDK 11``. JSQLParser-4.9 is the last ``JDK 8`` compatible release; everything after depends on ``JDK 11``. Building JSQLParser-5.1 and newer with Gradle needs a JDK 17 toolchain, because of the plugins used. + +.. tab:: Maven + + .. code-block:: shell + + git clone --depth 1 https://github.com/JSQLParser/JSqlParser.git + cd JSqlParser + mvn install + +.. tab:: Gradle + + .. code-block:: shell + + git clone --depth 1 https://github.com/JSQLParser/JSqlParser.git + cd JSqlParser + gradle publishToMavenLocal + +PostgreSQL roles, privileges and triggers +----------------------------------------- + +``CreateRole`` and ``AlterRole`` model role attributes and configuration changes, +including PostgreSQL's USER/GROUP aliases. Role options are ordered and typed; +passwords and configuration values are expressions. Omitted options remain +omitted. ``CREATE USER name`` without attributes retains the existing MySQL +``CreateUser`` AST by default. Select ``Dialect.POSTGRESQL`` explicitly to obtain +the PostgreSQL ``CreateRole`` AST for this ambiguous form:: + + CreateRole user = (CreateRole) CCJSqlParserUtil.parse( + "CREATE USER app", + parser -> parser.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL)); + +MySQL account names (``user[@host]``) of ``CREATE USER`` and of view, trigger and +routine ``DEFINER`` clauses share one grammar rule and are read into separate user +and host ``StringValue`` parts (``UserAccount`` and ``MySqlDefiner``). Every MySQL +spelling is accepted, including ``app@localhost``, ``app@'%'`` and +``'app'@localhost``. A part quoted with ``'``, ``"`` or a backquote keeps its quote +character in ``getQuoteStr()`` and its text in ``getValue()``:: + + CreateUser createUser = (CreateUser) CCJSqlParserUtil.parse( + "CREATE USER `app`@'%' IDENTIFIED BY 'secret'"); + UserAccount account = createUser.getAccounts().get(0); + account.getUser().getValue(); // app + account.getUser().getQuoteStr(); // ` + account.getHost().getValue(); // % + +``Grant`` retains its string-based getters, setters and fluent methods. Its +``PrivilegeClause`` exposes typed ``Privilege`` items, column lists, +``PrivilegeTarget`` kinds, multiple role memberships and grantor information. +``getPrivileges()`` is a mutable string view of the typed privilege list. +``getRole()`` and ``getObjectName()`` expose the first role or named target for +legacy callers; use ``getRoleItems()`` and ``getTarget()`` for the complete lists. +Names in ``PrivilegeTarget`` are multipart identifiers, not SQL clause text. +Routine targets use ``RoutineReference`` data-type signatures, with null +arguments for an omitted signature and an empty list for explicit ``()``. + +``Revoke`` shares the privilege payload and adds the revoked option and +CASCADE/RESTRICT behavior. ``AlterDefaultPrivileges`` has separate role/schema +scope and one nested GRANT or REVOKE. Table-name discovery reports explicit +table targets, not schema-wide targets, routine/type names or role names. + +``CreateTrigger`` supports both its existing MySQL statement body and a distinct +PostgreSQL routine invocation. PostgreSQL fields include multiple events, +UPDATE OF columns, constraint attributes, transition relations, row/statement +orientation and a WHEN expression. The absence of FOR ROW/STATEMENT is retained. +Expression visitors and ``StatementDeParser`` traverse privilege columns, +role values, trigger conditions and invocation arguments. Declaring a trigger +is classified as a schema change, not execution of its body. + +Capability validation covers these statement families, not every server-version +or catalog-dependent restriction. Event triggers use separate statements, described below. +Serialized role SQL can contain passwords; avoid logging real credentials. + +References: `CREATE ROLE `_, +`ALTER ROLE `_, +`GRANT `_, +`REVOKE `_, +`ALTER DEFAULT PRIVILEGES `_, +`CREATE TRIGGER `_. + +MySQL privileges and accounts in GRANT and REVOKE +------------------------------------------------- + +``Grant`` and ``Revoke`` also parse the MySQL forms, in every dialect: + +- **Privilege levels**: ``PrivilegeTarget.getWildcard()`` is ``DEFAULT_DATABASE`` for ``*``, + ``GLOBAL`` for ``*.*`` and ``DATABASE`` for ``db.*``, whose database is the single entry + of ``getNames()``. Named levels such as ``db.tbl`` keep a null wildcard. Only named + table targets are reported by table-name discovery. +- **Privileges**: MySQL static privileges are ``Privilege.Kind`` constants, for example + ``CREATE_TEMPORARY_TABLES`` or ``REPLICATION_SLAVE``; ``getName()`` returns the SQL + spelling. Another identifier, such as ``BACKUP_ADMIN``, is a dynamic privilege of kind + ``DYNAMIC`` with ``getDynamicName()``. +- **Grantees**: ``PrivilegeClause.getGranteeItems()`` returns ``Grantee`` values, which + MySQL and PostgreSQL share. ``ACCOUNT`` holds a ``UserAccount``: a MySQL ``user[@host]``, + read like the ``CREATE USER`` account names above, or a PostgreSQL role name without + host. The other kinds are + PostgreSQL ``GROUP name``, ``PUBLIC``, ``CURRENT_USER`` (MySQL also ``CURRENT_USER()``), + ``CURRENT_ROLE`` and ``SESSION_USER``. ``getGrantor()`` types ``GRANTED BY`` the same way. +- **Role memberships**: ``getRoleItems()`` holds the granted roles as ``UserAccount`` + values, so ``'reader'`` and ``'writer'@'%'`` keep their quoting and host. A membership + has no privileges and no target. +- **MySQL clauses**: ``PROXY ON user`` is a ``PROXY`` privilege on an ``ACCOUNT`` target + (``PrivilegeTarget.getAccount()``). ``REVOKE ALL [PRIVILEGES], GRANT OPTION FROM ...`` + has the privileges ``ALL`` and ``GRANT_OPTION`` and no target. ``Revoke`` records + ``IF EXISTS`` and ``IGNORE UNKNOWN USER``; ``Grant.getAsUser()`` and ``getWithRole()`` + hold ``AS user [WITH ROLE ...]``; the role selection is a ``RoleSelection`` from + ``net.sf.jsqlparser.statement.create.user``. + +.. code-block:: java + + Grant grant = (Grant) CCJSqlParserUtil.parse( + "GRANT SELECT, BACKUP_ADMIN ON db1.* TO 'app'@'%', CURRENT_USER"); + grant.getTarget().getWildcard(); // DATABASE + grant.getPrivilegeItems().get(1).getDynamicName(); // BACKUP_ADMIN + UserAccount app = grant.getClause().getGranteeItems().get(0).getAccount(); + app.getHost().getValue(); // % + +``Grant.getOptions()`` lists every ``WITH`` option, including PostgreSQL membership +options such as ``WITH INHERIT TRUE, SET FALSE``. Privileges take only ``GRANT OPTION``; +memberships take distinct ``ADMIN``, ``INHERIT`` and ``SET`` options. + +The string accessors are kept and derived from the typed state: ``getUsers()``, +``getGrantees()`` and ``getRoles()`` are mutable string views that write through, +``getOption()`` is the first option and ``getGrantedBy()`` renders the grantor. +``getObjectName()`` returns a wildcard level as written, for example ``db1.*``, and +``setObjectName()`` replaces it with a named target. Assigned text is converted with +``Grantee.valueOf()``: special role names are recognized and other text is kept as the +account name. Parsed memberships have no target; ``new Grant()`` still starts with an +empty table target, and ``setRole()`` clears it. + +The MySQL and MariaDB validation capabilities now include ``revoke``. + +References: `GRANT `_, +`REVOKE `_, +`Specifying Account Names `_. + +PostgreSQL event triggers, REINDEX, SECURITY LABEL and schema elements +---------------------------------------------------------------------- + +``CREATE EVENT TRIGGER`` parses to ``CreateEventTrigger``. ``getEvent()`` keeps the event +name as written and ``getFilters()`` the ``WHEN … IN (…)`` conditions in source order. The +invoked function reuses ``CreateTrigger.ExecuteKeyword`` and a ``Function`` without +arguments. ``ALTER EVENT TRIGGER`` parses to ``AlterEventTrigger``; its states reuse +``RelationAlterAction.EnableState``, and each non-null setter selects the ``STATE``, +``OWNER`` or ``RENAME`` action. A MySQL event named ``trigger`` still parses to +``CreateEvent`` or ``AlterEvent``. + +.. code-block:: java + + CreateEventTrigger trigger = (CreateEventTrigger) CCJSqlParserUtil.parse( + "CREATE EVENT TRIGGER etg ON ddl_command_start " + + "WHEN TAG IN ('CREATE TABLE') EXECUTE FUNCTION abort_any_command()"); + trigger.getEvent(); // ddl_command_start + trigger.getFilters().get(0).getVariable(); // TAG + trigger.getFunction().getName(); // abort_any_command + +``REINDEX`` parses to ``Reindex``: a ``Target`` (INDEX, TABLE, SCHEMA, DATABASE or SYSTEM), +the ``CONCURRENTLY`` keyword, the parenthesized ``CONCURRENTLY``, ``TABLESPACE`` and +``VERBOSE`` options with their values as written, and the object name. Only DATABASE and +SYSTEM may omit the name. ``getTable()`` returns the name only for ``REINDEX TABLE``, which +table visitors report. ``REINDEX`` is a non-reserved keyword. + +``SECURITY LABEL`` parses to ``SecurityLabel`` with the provider as written and one target, +told apart by ``getObjectType()``: a named object (``getObjectName()``), a qualified column +(``getColumn()``), a FUNCTION, PROCEDURE, ROUTINE or AGGREGATE signature (``getRoutine()``) +or a large object OID. A null label stands for ``IS NULL``. Table visitors report tables, +views, materialized views, foreign tables and a column's table. + +``CREATE SCHEMA`` keeps its schema elements in ``getStatements()`` and renders them after the +``AUTHORIZATION`` and ``PATH`` clauses. The element kinds are PostgreSQL's: ``CREATE TABLE``, +``CREATE VIEW``, ``CREATE INDEX``, ``CREATE SEQUENCE``, ``CREATE TRIGGER`` and ``GRANT``; +``CREATE SCHEMA IF NOT EXISTS`` cannot include them. Elements keep their names as written, +and unqualified names refer to the new schema. The parser no longer copies the schema name +into ``CREATE TABLE`` elements. Table visitors, validation and ``StatementDeParser`` traverse +the elements. + +Validators report ``createEventTrigger``, ``alterEventTrigger``, ``reindex``, +``reindexConcurrently`` (PostgreSQL 12) and ``securityLabel``. Feature analysis classifies +these statements as schema changes; ``REINDEX`` also reads data. + +References: `CREATE EVENT TRIGGER `_, +`ALTER EVENT TRIGGER `_, +`REINDEX `_, +`SECURITY LABEL `_, +`CREATE SCHEMA `_. + +Migrating shared ALTER enums +---------------------------- + +``AlterServer.Action`` and ``AlterForeignDataWrapper.Action`` have been replaced +by ``ForeignObjectAlterAction`` in ``net.sf.jsqlparser.statement.alter``. Both +statements' ``getAction()`` and ``setAction()`` now use this enum, with the same +``OPTIONS``, ``OWNER`` and ``RENAME`` constants. Update imports and recompile code +using these APIs; the removed nested enums are not compatibility aliases. + +.. code-block:: java + + AlterServer server = (AlterServer) CCJSqlParserUtil.parse( + "ALTER SERVER s VERSION NULL"); + server.setAction(ForeignObjectAlterAction.OWNER); + server.setOwner("CURRENT_USER"); + // ALTER SERVER s OWNER TO CURRENT_USER + + AlterForeignDataWrapper wrapper = (AlterForeignDataWrapper) CCJSqlParserUtil.parse( + "ALTER FOREIGN DATA WRAPPER w NO HANDLER"); + wrapper.setAction(ForeignObjectAlterAction.OWNER); + wrapper.setOwner("CURRENT_USER"); + // ALTER FOREIGN DATA WRAPPER w OWNER TO CURRENT_USER + +``OPTIONS`` remains the initial action. It includes server ``VERSION`` changes +and wrapper handler/validator changes as well as foreign-data options. These +payloads remain on their respective statement classes. Switching to ``OWNER`` or +``RENAME`` leaves the inactive payload available for switching back to ``OPTIONS``. + +``RelationAlterAction.TriggerState``, ``getTriggerState()`` and +``setTriggerState()`` have been removed. Use the existing +``RelationAlterAction.EnableState``, ``getEnableState()`` and ``setEnableState()`` +for both trigger and rewrite-rule actions: + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ENABLE REPLICA TRIGGER trg", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + RelationAlterAction trigger = (RelationAlterAction) alter.getAlterExpressions().get(0); + trigger.setEnableState(RelationAlterAction.EnableState.DISABLE); + // ALTER TABLE t DISABLE TRIGGER trg + +The action's ``Kind`` still distinguishes ``TRIGGER_STATE`` from ``RULE_STATE``. +Trigger targets also retain their ``NAME``, ``ALL`` and ``USER`` distinctions. + +Oracle anonymous blocks +----------------------- + +With ``Dialect.ORACLE``, ``OracleBlock`` extends ``Block`` with variable declarations +and exception handlers. Initializers and ``OracleAssignment`` values are expressions; +nested blocks and handler bodies contain statements. Calls without ``CALL`` use +``Execute.ExecType.IMPLICIT``, preserving qualified names, parentheses and bind arguments. +``OracleNullStatement`` represents the PL/SQL ``NULL`` statement. Implicit calls are +recognized inside Oracle blocks, so application procedure names need no keyword registration. + +Shared traversal and rendering include declaration initializers, assignments and exception +handler bodies. Procedure side effects remain unknown; table discovery reports unsupported +procedure calls, and feature analysis remains conservative. This covers anonymous blocks +with variable declarations, SQL statements, assignments, calls, nesting and handlers, not +all PL/SQL declarations, loops, packages or procedure definitions. + +SQL Server routine declarations +------------------------------- + +``Dialect.SQLSERVER`` uses a shared declaration path for ``CREATE``, ``ALTER`` and +``CREATE OR ALTER FUNCTION/PROCEDURE``. ``CreateFunctionalStatement.getOperation()`` +identifies the operation. For functions, ``getReturnType()`` exposes scalar types, +inline ``RETURNS TABLE``, and a return variable with ordered ``TableElement`` column +and constraint definitions. Table elements reuse the existing definition traversal +and deparser, including custom expression visitors. + +With a structured return type, ``getFunctionDeclarationParts()`` contains the name +and parameter tokens; ``getRoutineBodyParts()`` contains the following options and +body. These remain opaque tokens, so this does not implement a T-SQL body AST or +resolve tables used inside a routine. Other dialects retain the existing token-list +representation. New operations have separate validation capabilities. + +Parse procedure definitions one SQL Server batch at a time: a procedure consumes the +remaining batch, including SQL after an ``END``. Client-side ``GO`` batch splitting is +not performed by this routine declaration parser. + +PostgreSQL table-level NOT NULL constraints +------------------------------------------ + +``Dialect.POSTGRESQL`` supports PostgreSQL 18's table-level +``CONSTRAINT nn NOT NULL id``, in both CREATE TABLE and ALTER TABLE ADD. +``NotNullConstraint`` exposes the constraint name, target ``Column`` and +``noInherit`` flag; ``getConstraintAttributes().isNotValid()`` represents an +ALTER ``NOT VALID`` clause when present. + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NOT VALID", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + NotNullConstraint constraint = (NotNullConstraint) + alter.getAlterExpressions().get(0).getConstraint(); + constraint.getColumn().setColumnName("other_id"); + constraint.setName("other_nn"); + +The target column participates in expression visitors and deparsers. New nodes +can be built with ``new NotNullConstraint().withName("nn") +.withColumn(new Column("id"))``. This is distinct from column definitions and +``ALTER COLUMN ... SET NOT NULL``; those retain their existing APIs. + +MySQL and PostgreSQL tablespaces +-------------------------------- + +``CREATE TABLESPACE`` parses to ``CreateTablespace`` and ``ALTER TABLESPACE`` to +``AlterTablespace`` in every dialect; ``isUndo()`` records the MySQL ``UNDO`` keyword. +A PostgreSQL definition exposes ``getOwner()``, ``getLocation()`` and the ``WITH`` +parameters as ``IndexOption`` values in ``getParameters()``. A MySQL definition exposes +``getDataFile()`` and keeps its options in source order in ``getOptions()``. They reuse the +``TableOption`` kinds ``ENGINE``, ``ENCRYPTION``, ``AUTOEXTEND_SIZE`` and +``ENGINE_ATTRIBUTE``; CREATE also accepts ``FILE_BLOCK_SIZE``. + +.. code-block:: java + + CreateTablespace create = (CreateTablespace) CCJSqlParserUtil.parse( + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' FILE_BLOCK_SIZE = 8192 ENGINE = InnoDB"); + create.getDataFile().getValue(); // ts1.ibd + create.getOption(TableOption.Kind.ENGINE).get().getValue(); // InnoDB + + AlterTablespace alter = (AlterTablespace) CCJSqlParserUtil.parse( + "ALTER TABLESPACE fast SET (random_page_cost = 1.1)"); + alter.getAction(); // SET_PARAMETERS + alter.getParameters(); // [random_page_cost = 1.1] + +``getAction()`` is ``RENAME``, ``OWNER``, ``SET_PARAMETERS`` or ``RESET_PARAMETERS``, +``SET_STATE`` for ``ALTER UNDO TABLESPACE … SET ACTIVE|INACTIVE``, or ``OPTIONS`` for MySQL +options. Undo tablespaces accept only ``ENGINE``, ``CREATE UNDO TABLESPACE`` requires +``ADD DATAFILE``, and ALTER rejects ``FILE_BLOCK_SIZE``. Other ``ALTER TABLESPACE`` forms, +such as Oracle and NDB data file clauses, still parse as ``UnsupportedStatement``, and +``ALTER TABLE ALL IN TABLESPACE`` remains ``AlterTablespaceMove``. Validators report the +``createTablespace`` and ``alterTablespace`` features, which the MySQL and PostgreSQL +capabilities enable; feature analysis reports ``MODIFIES_SCHEMA``. + +SQL Server identity inserts +--------------------------- + +With ``Dialect.SQLSERVER``, ``SET IDENTITY_INSERT dbo.actor ON`` uses +``SetIdentityInsertStatement``. ``getTable()`` reuses the qualified ``Table`` AST; +``isOn()`` and ``setOn()`` expose the session setting. Table names may include a +database and schema, including SQL Server bracket-quoted identifiers. Table +visitors, both SQL renderers and metadata validation use this structured target. +Feature analysis reports ``MODIFIES_SESSION``; the directive itself inserts no rows. +The dedicated validation capability is ``setIdentityInsert``. + +Database options and configuration parameters +--------------------------------------------- + +``CreateDatabase``, MySQL's ``CREATE SCHEMA`` and ``AlterDatabase`` share the +``DatabaseOption`` model. MySQL options are ``CHARACTER_SET`` (``getName()`` keeps +the ``CHARACTER SET``, ``CHAR SET`` or ``CHARSET`` spelling), ``COLLATE``, +``ENCRYPTION`` and, for ALTER only, ``READ_ONLY``. PostgreSQL options have one kind +each, from ``OWNER`` and ``TEMPLATE`` to ``CONNECTION_LIMIT``, ``IS_TEMPLATE`` and +``OID``; ``isUseWith()`` records the optional ``WITH``. Values keep their SQL text, +such as ``'UTF8'`` or ``-1``. Other option text remains one raw ``OTHER`` option. + +``AlterDatabase.getAction()`` selects what is rendered: ``OPTIONS`` (the default), +``RENAME``, ``OWNER``, ``SET_TABLESPACE``, ``REFRESH_COLLATION_VERSION`` or +``CONFIGURATION_PARAMETER``. Payloads of the other actions are kept. ``SET`` and +``RESET`` of a configuration parameter use ``ConfigurationParameterChange``, which +``AlterRole`` shares: + +.. code-block:: java + + AlterDatabase alter = (AlterDatabase) CCJSqlParserUtil.parse( + "ALTER DATABASE app SET search_path TO app, public", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + ConfigurationParameterChange change = alter.getParameterChange(); + change.getParameter(); // search_path + change.getValues(); // [app, public], a mutable ExpressionList + change.setValueKind(ConfigurationParameterChange.ValueKind.DEFAULT); + // ALTER DATABASE app SET search_path TO DEFAULT + +``getValueKind()`` is ``VALUES``, ``DEFAULT`` or ``FROM_CURRENT``; ``RESET ALL`` uses +the parameter name ``ALL``. ``AlterRole`` keeps ``getParameter()``, ``isUseEquals()``, +``isFromCurrent()``, ``isUseDefault()``, ``getValues()`` and their setters; they read +and edit ``getParameterChange()``. ``FROM CURRENT`` and ``DEFAULT`` are now mutually +exclusive: ``setFromCurrent(false)`` and ``setUseDefault(false)`` return to the values. + +Without the MySQL dialect, ``ALTER DATABASE`` forms that are neither MySQL nor +PostgreSQL syntax, such as ``ALTER DATABASE d SET RECOVERY SIMPLE``, remain an +``UnsupportedStatement``; ``Dialect.MYSQL`` reports them as errors. ``ALTER SCHEMA`` +followed by MySQL options is an ``AlterDatabase`` in every dialect, while PostgreSQL's +``ALTER SCHEMA ... RENAME TO/OWNER TO`` remains an ``AlterSchema``. Validation +reports the ``createDatabase`` and ``alterDatabase`` features, which the MySQL, +MariaDB and PostgreSQL capabilities enable. + +``CreateDatabase.getOptions()`` is now a live list that is never null, and +``setOptions`` copies its argument. ``getDatabaseOptions()`` is an unmodifiable +token snapshot derived from the options, including ``WITH``, and is null without +options. ``setDatabaseOptions`` replaces all options with one raw option, and +``addDatabaseOptions`` appends one while keeping the typed options. Parsed +statements no longer end with an empty raw option. + +References: MySQL `CREATE DATABASE `_, +`ALTER DATABASE `_; +PostgreSQL `CREATE DATABASE `_, +`ALTER DATABASE `_. + +Routine and trigger alterations +------------------------------- + +PostgreSQL ``ALTER FUNCTION``, ``ALTER PROCEDURE`` and ``ALTER ROUTINE`` and MySQL +``ALTER FUNCTION`` and ``ALTER PROCEDURE`` parse to ``AlterRoutine``. ``getRoutineType()`` +names the keyword and ``getRoutine()`` is a ``RoutineReference`` with the optional +PostgreSQL argument types. ``getAction()`` is ``CHARACTERISTICS``, ``RENAME``, ``OWNER``, +``SET_SCHEMA`` or ``DEPENDS_ON_EXTENSION`` (``isNoDependency()`` for ``NO DEPENDS``); +payloads of the other actions are kept. ``getCharacteristics()`` is a mutable list of +``RoutineCharacteristic`` in source order, shared by both dialects. Each ``Kind`` renders +its own keywords, such as ``IMMUTABLE``, ``CALLED_ON_NULL_INPUT``, ``PARALLEL_SAFE``, +``COST`` or MySQL's ``READS_SQL_DATA``. ``SECURITY_DEFINER`` and ``SECURITY_INVOKER`` keep +the ``[EXTERNAL] SECURITY`` or ``SQL SECURITY`` spelling in ``getSecurityClause()``. +``COST``, ``ROWS``, ``SUPPORT`` and ``COMMENT`` keep their value as SQL text in ``getValue()``. +``getComment()`` returns the ``COMMENT`` literal as a ``StringValue`` read with the dialect's +escaping, so ``getNotExcapedValue()`` is the text MySQL stores. ``SET`` and ``RESET`` reuse +``ConfigurationParameterChange``: + +.. code-block:: java + + AlterRoutine alter = (AlterRoutine) CCJSqlParserUtil.parse( + "ALTER FUNCTION f(int) IMMUTABLE SET search_path = public", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + alter.getRoutine().getArguments(); // [int] + alter.getCharacteristics().get(0).getKind(); // IMMUTABLE + alter.getCharacteristics().get(1).getParameterChange().getParameter(); // search_path + +The characteristics of one statement belong to one dialect, and MySQL's take no argument +list. Other forms after ``ALTER FUNCTION`` or ``ALTER PROCEDURE``, such as Oracle's +``COMPILE`` or SQL Server routine definitions without the SQL Server dialect, remain an +``UnsupportedStatement``. PostgreSQL's ``ALTER TRIGGER name ON table RENAME TO`` and +``[NO] DEPENDS ON EXTENSION`` parse to ``AlterTrigger``; table visitors report its table. +Validators report ``alterFunction``, ``alterProcedure``, ``alterRoutine`` and +``alterTrigger``; PostgreSQL enables all of them, MySQL and MariaDB the first two. + +``CreateFunctionalStatement.getDefiner()`` returns the MySQL ``DEFINER`` of +``CREATE DEFINER = ... FUNCTION|PROCEDURE`` as the ``MySqlDefiner`` that views and +triggers use. With ``Dialect.MYSQL`` or ``Dialect.MARIADB``, +``CreateFunctionalStatement.getCharacteristics()`` lists the characteristics between the +routine header and its body, such as ``COMMENT``, ``DETERMINISTIC``, ``READS SQL DATA`` or +``SQL SECURITY INVOKER``, with the same ``RoutineCharacteristic`` kinds; ``DETERMINISTIC`` +and ``NOT_DETERMINISTIC`` occur only here. ``getFunctionDeclarationParts()`` still holds +every token and renders the statement unchanged, so the list is read-only. Replacing the +tokens with ``setFunctionDeclarationParts(parts)`` clears it, while +``setFunctionDeclarationParts(parts, characteristics)`` sets both, and appending tokens +keeps it. Without a MySQL dialect the list stays empty. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``) +and keeps MariaDB's ``OR REPLACE``. A ``BEGIN ... END`` trigger body with only SQL +statements remains a ``Block``; a compound body with procedural statements such as +``IF``, ``DECLARE`` or loops is an ``UnsupportedStatement`` with its tokens, captured with +the boundary of routine bodies. In both cases the semicolon after ``END`` separates the +next statement. + +References: PostgreSQL `ALTER FUNCTION `_, +`ALTER PROCEDURE `_, +`ALTER ROUTINE `_, +`ALTER TRIGGER `_; +MySQL `ALTER FUNCTION `_, +`ALTER PROCEDURE `_, +`CREATE TRIGGER `_. + +MySQL table maintenance statements +---------------------------------- + +``CHECK TABLE``, ``CHECKSUM TABLE``, ``OPTIMIZE TABLE`` and ``REPAIR TABLE`` parse to +``TableMaintenanceStatement`` in every dialect; ``ANALYZE TABLE`` remains ``Analyze``. +``getOperation()`` identifies the statement, ``getModifier()`` returns +``NO_WRITE_TO_BINLOG`` or ``LOCAL`` for OPTIMIZE and REPAIR, and ``isTablesKeyword()`` +records the ``TABLES`` spelling. ``getOptions()`` keeps the trailing options in source +order. CHECK accepts ``FOR UPGRADE``, ``QUICK``, ``FAST``, ``MEDIUM``, ``EXTENDED`` and +``CHANGED``; CHECKSUM accepts one of ``QUICK`` and ``EXTENDED``; REPAIR accepts ``QUICK``, +``EXTENDED`` and ``USE_FRM``. Other combinations are rejected, and the option words and +``CHECKSUM`` remain ordinary identifiers elsewhere. + +.. code-block:: java + + TableMaintenanceStatement repair = (TableMaintenanceStatement) CCJSqlParserUtil.parse( + "REPAIR NO_WRITE_TO_BINLOG TABLE t1, t2 QUICK"); + repair.getTables(); // [t1, t2] + repair.getOptions(); // [QUICK] + +Table visitors and metadata validation use the table list. Validators report the +``tableMaintenance`` feature, which the MySQL and MariaDB capabilities enable. Feature +analysis reports ``READS_DATA`` and ``RETURNS_RESULT_SET``; OPTIMIZE and REPAIR also +report ``MODIFIES_SCHEMA``, and REPAIR may report ``MODIFIES_DATA``. + +``ANALYZE TABLE ... UPDATE HISTOGRAM ON`` keeps ``WITH n BUCKETS`` +(``getHistogramBucketCount()``), MySQL 8.4's ``AUTO UPDATE`` or ``MANUAL UPDATE`` +(``getHistogramUpdateMode()``) and ``USING DATA 'json'`` (``getHistogramData()``, a +``StringValue``). ``USING DATA`` excludes the other two options, and ``DROP HISTOGRAM`` +takes none of them. + +MySQL account management statements +----------------------------------- + +``CREATE USER`` and ``ALTER USER`` share ``UserOptions`` (``getOptions()``) in MySQL's +clause order: the ``DEFAULT ROLE`` roles, the ``REQUIRE`` options (``TlsOption``), the +``WITH`` resource limits (``ResourceLimit``), the password and locking options +(``PasswordLockOption``) in source order, and ``COMMENT`` or ``ATTRIBUTE``. Omitted +clauses stay omitted. ``UserAuthentication`` adds ``ALTER USER``'s ``REPLACE 'current'`` +(``getCurrentCredential()``), ``RETAIN CURRENT PASSWORD`` and the +``DISCARD_OLD_PASSWORD`` mode. + +.. code-block:: java + + AlterUser alter = (AlterUser) CCJSqlParserUtil.parse( + "ALTER USER 'app'@'%' IDENTIFIED BY 'new' RETAIN CURRENT PASSWORD " + + "PASSWORD EXPIRE INTERVAL 90 DAY ACCOUNT LOCK"); + alter.getAccounts().get(0).getAuthentication().isRetainCurrentPassword(); // true + alter.getOptions().getPasswordLockOptions(); // [PASSWORD EXPIRE INTERVAL 90 DAY, ACCOUNT LOCK] + +``ALTER USER USER() ...`` keeps ``USER()`` as the unquoted account name, as +``DEFINER = CURRENT_USER`` does; ``isUserFunction()`` identifies it. +``ALTER USER acct DEFAULT ROLE {NONE | ALL | role, ...}`` stores a ``RoleSelection`` in +``getOptions().getDefaultRoles()``. PostgreSQL's ``ALTER USER``, an alias of ``ALTER ROLE`` +(``WITH LOGIN``, ``PASSWORD 'x'``, ``SET``, ``RENAME TO`` ...), still parses to +``AlterRole``. MySQL syntax is recognized by ``IF EXISTS``, ``USER()``, quoted names, +hosts, account lists, a missing action or a MySQL account clause. + +``RENAME USER`` parses to ``RenameUser`` with ordered ``from``/``to`` pairs. MySQL +``CREATE ROLE [IF NOT EXISTS] role, ...`` with quoted names, hosts or several roles keeps +them in ``CreateRole.getRoles()``, and ``getName()`` is null. A single unquoted name, which +both databases accept, keeps the PostgreSQL ``getName()``; so does an unquoted ``r@h``, which +the lexer reads as one identifier, unless the MySQL or MariaDB dialect is selected. Role names +use the shared account spelling and are ``UserAccount`` values without authentication. + +``SET DEFAULT ROLE`` (MySQL's ``TO``, MariaDB's ``FOR``) and ``SET PASSWORD FOR`` parse to +``SetDefaultRoleStatement`` and ``SetPasswordStatement`` in every dialect. ``SET ROLE`` and +``SET PASSWORD`` without ``FOR`` share the variable assignment shape that PostgreSQL uses +for ``SET ROLE name`` and ``SET ROLE NONE``, so they remain a ``SetStatement`` unless +``Dialect.MYSQL`` or ``Dialect.MARIADB`` is selected. With these dialects every form parses +to ``SetRoleStatement`` (``DEFAULT``, ``NONE``, ``ALL [EXCEPT ...]`` or a role list) or +``SetPasswordStatement`` (``= password`` or ``TO RANDOM``, then ``REPLACE`` and +``RETAIN CURRENT PASSWORD``). The MySQL-only ``SET ROLE ALL EXCEPT ...`` and role names +with a quoted user or host, such as ``'r'@'%'``, parse to ``SetRoleStatement`` in every +dialect. + +.. code-block:: java + + SetRoleStatement setRole = (SetRoleStatement) CCJSqlParserUtil.parse( + "SET ROLE ALL EXCEPT 'admin'@'%'", + parser -> parser.withDialect(Dialect.MYSQL)); + setRole.getRoles().getKind(); // ALL_EXCEPT + setRole.getRoles().getRoles(); // ['admin'@'%'] + +Validators report ``alterUser``, ``renameUser``, ``setRole``, ``setDefaultRole`` and +``setPassword``; the MySQL and MariaDB capabilities enable them and ``createRole``. +Feature analysis reports ``MODIFIES_SCHEMA``, or ``MODIFIES_SESSION`` for ``SET ROLE``. +Multi-factor authentication clauses are not modeled. Serialized account SQL can contain +passwords; avoid logging real credentials. + +Legacy MySQL GROUP BY ordering +============================== + +MySQL before 8.0.13 accepted ``ASC`` and ``DESC`` on individual ``GROUP BY`` items. +Select the existing ``MYSQL`` dialect and explicitly enable this legacy syntax: + +.. code-block:: java + + Statement statement = CCJSqlParserUtil.parse( + "SELECT a FROM t GROUP BY a DESC", + parser -> parser.withDialect(Dialect.MYSQL).withLegacyMySqlGroupBy(true)); + +The option is disabled by default and does not enable this syntax in other dialects. +``GroupByElement`` keeps its existing expression list; ``getGroupBySortDirection(index)`` +returns each explicit direction, or null when omitted. Directions follow list positions; +replacing the expression list clears them. Validators report the separate +``selectGroupByOrdering`` feature, which is not enabled in the MySQL 8.0 capability. + +PostgreSQL COMMENT targets +========================= + +``COMMENT ON`` supports these PostgreSQL object kinds in addition to the existing +``TABLE``, ``COLUMN`` and ``VIEW`` forms: + +- ``INDEX``, ``SCHEMA``, ``SEQUENCE``, ``DOMAIN``, ``TYPE``, ``MATERIALIZED VIEW``, + ``FOREIGN TABLE``, ``COLLATION``, ``STATISTICS``, ``CONVERSION`` and + ``TEXT SEARCH {CONFIGURATION | DICTIONARY | PARSER | TEMPLATE}`` +- ``ACCESS METHOD``, ``DATABASE``, ``EVENT TRIGGER``, ``EXTENSION``, + ``FOREIGN DATA WRAPPER``, ``[PROCEDURAL] LANGUAGE``, ``PUBLICATION``, ``ROLE``, + ``SERVER``, ``SUBSCRIPTION`` and ``TABLESPACE`` +- ``FUNCTION``, ``PROCEDURE``, ``ROUTINE`` and ``AGGREGATE`` signatures +- ``CONSTRAINT``, ``TRIGGER``, ``POLICY`` and ``RULE`` with their owning table +- ``CAST (source_type AS target_type)`` +- ``OPERATOR [schema.]op (left_type, right_type)``, where ``NONE`` marks the missing + operand of a unary operator +- ``OPERATOR CLASS`` and ``OPERATOR FAMILY`` with ``USING access_method`` +- ``LARGE OBJECT oid`` and ``TRANSFORM FOR type_name LANGUAGE lang_name`` + +These unambiguous target forms also parse without a dialect preset. Kind words such as +``ROLE``, ``SERVER``, ``RULE``, ``OPERATOR`` or ``CONVERSION`` are not reserved and +remain ordinary identifiers. +Tagged dollar strings still require ``Dialect.POSTGRESQL`` or +``withDollarQuotedStringTags(true)``. + +The additional targets are exposed through ``Comment.getTarget()`` as a +``CommentTarget``. Its ``Kind`` identifies the object, and ``getName()`` preserves +the individual identifier components. ``Table`` is used as the name container; +an index or type name does not thereby represent a table dependency. +``isProcedural()`` records ``PROCEDURAL LANGUAGE``. +The original ``Comment.getTable()``, ``getColumn()`` and ``getView()`` accessors +continue to describe their respective existing forms. + +``COMMENT ON FUNCTION app.f(IN value integer) IS 'description'`` uses the shared +``RoutineReference`` in ``getTarget().getRoutine()``; ``Kind.isRoutine()`` identifies +these kinds and ``getName()`` is null. Argument mode, optional name +and ``ColDataType`` are preserved. An omitted signature has null arguments; +``f()`` has an empty argument list. The signature identifies a routine and is not +a function call. An ``AGGREGATE`` requires a signature and also accepts ``(*)`` and +ordered-set signatures such as ``(float8 ORDER BY integer)`` or ``(ORDER BY integer)``. + +For ``COMMENT ON CONSTRAINT ck ON app.t IS NULL``, ``getName()`` is the constraint +name and ``getRelation()`` is its owning table. ``TRIGGER``, ``POLICY`` and ``RULE`` +targets use the same fields; ``Kind.isOwnedByRelation()`` identifies these kinds. +Only a constraint accepts ``ON DOMAIN app.d``, which sets ``isOnDomain()`` and stores +the domain as the owner instead. A ``CAST`` target exposes ``getSourceType()`` and +``getTargetType()``. ``NULL`` removes the comment and remains represented by a null +``Comment.getComment()``. + +``COMMENT ON OPERATOR app.-> (jsonb, NONE)`` reads the operator with the reference +shared by exclusion constraints and ``ALTER EXTENSION``: ``getOperator()`` returns an +``ExclusionOperator`` with ``getSchemaName()`` and the operator symbol in ``getName()``. +``getLeftOperandType()`` and ``getRightOperandType()`` hold the operand types, and null +stands for ``NONE``; at least one operand type is required. The lexer reads some +PostgreSQL operators, such as ``<<=`` or ``-|-``, as several tokens. Parts that touch +each other are joined into one operator, while whitespace or a comment separates two +operators as in PostgreSQL. Exclusion constraints and ``ALTER EXTENSION`` accept the same +operators. Operators that the lexer reads as part of an identifier, such as ``##`` +without ``Dialect.POSTGRESQL``, are not supported. + +``OPERATOR CLASS`` and ``OPERATOR FAMILY`` targets keep their name in ``getName()`` +and the index access method in ``getAccessMethod()``; ``Kind.isOwnedByAccessMethod()`` +identifies these kinds. ``LARGE OBJECT`` exposes ``getLargeObjectOid()``, which +accepts an unsigned 32-bit integer. ``TRANSFORM`` exposes ``getTransformType()`` and +``getLanguage()``. Each ``TEXT SEARCH`` object type has its own ``Kind``, for example +``TEXT_SEARCH_DICTIONARY``. + +Table discovery visits tables, column owners, views, materialized views, foreign +tables and the tables owning constraints, triggers, policies and rules. It does not +infer an index's table or treat domains, sequences, types, routines or other catalog +objects as tables. Statement visitors visit comment literals, and custom SQL deparsers +can replace the explicit relation or literal. Feature analysis reports a schema +modification. Validation exposes a separate ``commentOn...`` capability for each +additional target kind. + +Current date/time expression metadata +===================================== + +``TemporalExpressionInfo.from(expression, dialect)`` provides a common read-only +view of MySQL and PostgreSQL current-date/time expressions. It recognizes the +existing ``TimeKeyExpression``, ``Function`` and ``Column`` representations without +replacing nodes or changing their SQL rendering: + +.. code-block:: java + + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT CURRENT_TIMESTAMP(6)", p -> p.withDialect(Dialect.POSTGRESQL)); + TemporalExpressionInfo info = TemporalExpressionInfo.from( + select.getSelectItem(0).getExpression(), Dialect.POSTGRESQL).orElseThrow(); + // info.getKind() == TemporalExpressionInfo.Kind.CURRENT_TIMESTAMP + // info.getPrecision() == 6 + +Import ``TemporalExpressionInfo`` from ``net.sf.jsqlparser.util``. Precision is +``null`` when omitted, and explicit zero is preserved. ``getName()`` retains the +original keyword or function name; ``hasParentheses()`` distinguishes bare and +call forms. Results are snapshots: call ``from`` again after editing an AST. + +The dialect is significant. MySQL ``LOCALTIME`` and ``LOCALTIMESTAMP`` are aliases +of ``CURRENT_TIMESTAMP``; PostgreSQL exposes ``LOCAL_TIME`` and +``LOCAL_TIMESTAMP`` separately. MySQL ``NOW``, ``CURDATE`` and ``CURTIME`` aliases +are recognized in their function-call forms. PostgreSQL ``now()`` is recognized +without precision arguments. Quoted or qualified identifiers, unrelated +expressions and unsupported dialects return ``Optional.empty()``. + +This API identifies expression metadata rather than performing database +validation. Precision reports the requested value, without applying defaults, +server range checks or clamping. For example, PostgreSQL accepts precision 7 with +a warning and clamps it to 6, whereas MySQL rejects it. + +Attach a PostgreSQL constraint to an existing index +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With ``Dialect.POSTGRESQL``, ``ALTER TABLE ... ADD UNIQUE USING INDEX`` and +``ADD PRIMARY KEY USING INDEX`` expose a ``ConstraintUsingIndex`` through +``AlterExpression.getConstraint()``. Its ``getName()`` is the optional new constraint +name, while ``getExistingIndexName()`` identifies the existing index. This is +separate from an index declaration's name, columns and access method. + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + ConstraintUsingIndex constraint = (ConstraintUsingIndex) + alter.getAlterExpressions().get(0).getConstraint(); + constraint.setExistingIndexName("replacement_index"); + constraint.setName("replacement_constraint"); + +``getConstraintAttributes()`` exposes deferrability and initial timing when +present. The same AST can be constructed with ``new ConstraintUsingIndex()`` +and its fluent ``withName``, ``withType`` and ``withExistingIndexName`` methods. + +Transaction statements +====================== + +``START TRANSACTION`` produces a ``StartTransaction`` with an ordered list of typed +transaction modes. PostgreSQL isolation levels, ``READ ONLY`` / ``READ WRITE``, and +``[NOT] DEFERRABLE`` are supported, as is MySQL ``WITH CONSISTENT SNAPSHOT``. +PostgreSQL accepts mode lists with or without commas; output uses commas. +The explicit MySQL dialect rejects a transaction containing both ``READ ONLY`` and +``READ WRITE``, while retaining repeated occurrences of the same mode. + +Choose the dialect explicitly to parse ``BEGIN`` as a transaction command: + +.. code-block:: java + + StartTransaction transaction = (StartTransaction) CCJSqlParserUtil.parse( + "BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + transaction.getModes().set(1, StartTransaction.Mode.READ_WRITE); + // BEGIN ISOLATION LEVEL SERIALIZABLE, READ WRITE + String sql = transaction.toString(); + +Without a dialect, ``BEGIN`` retains the existing procedural block interpretation. +MySQL and PostgreSQL dialects distinguish ``BEGIN`` transactions from +``BEGIN SELECT ...; END`` blocks by the following tokens. Oracle blocks and opaque +routine bodies retain their existing handling. + +``Commit`` and ``RollbackStatement`` expose optional ``TransactionKeyword`` +(``WORK`` or PostgreSQL ``TRANSACTION``) and ``TransactionChain`` (``CHAIN`` or +``NO_CHAIN``). A null chain preserves the server's default behavior. The existing +rollback work/savepoint/force accessors remain available. ``RELEASE [SAVEPOINT]`` +produces ``ReleaseSavepointStatement``; the MySQL dialect requires ``SAVEPOINT``. +Savepoint names retain identifier quoting. These statements are traversed by +statement visitors, deparsers and transaction feature analysis. + +The validator checks the new statement and mode capabilities. PostgreSQL +``AND [NO] CHAIN`` is enabled beginning with its version 12 capability set. +This support does not include MySQL transaction completion ``[NO] RELEASE``, +PostgreSQL ``COMMIT PREPARED`` / ``ROLLBACK PREPARED``, or XA commands. + +MySQL SHOW statements +===================== + +The MySQL statements that read table, column and object definitions parse to typed +statements in every dialect: + +* ``SHOW [EXTENDED] [FULL] {COLUMNS | FIELDS} {FROM | IN} t [{FROM | IN} db] + [LIKE 'p' | WHERE expr]`` extends ``ShowColumnsStatement``. ``getTable()`` returns the + ``Table``; ``getTableName()`` and ``setTableName(String)`` remain and are derived from it. + ``isExtended()``, ``isFull()`` and ``isFieldsKeyword()`` record the modifiers and the + ``FIELDS`` spelling, ``getTableSelectionMode()`` and ``getDbSelectionMode()`` the + ``FROM``/``IN`` spelling, and ``getDbName()``, ``getLikeExpression()`` and + ``getWhereCondition()`` the optional clauses. +* ``SHOW TABLE STATUS [{FROM | IN} db] [LIKE 'p' | WHERE expr]`` parses to + ``ShowTableStatusStatement``, with the same accessors as ``ShowTablesStatement``. +* ``SHOW CREATE`` parses to ``ShowCreateStatement``. ``getObjectType()`` is ``TABLE``, + ``VIEW``, ``PROCEDURE``, ``FUNCTION``, ``TRIGGER``, ``EVENT``, ``DATABASE`` or ``USER``. + The first six name the object with ``getName()``, a ``Table``. ``DATABASE`` uses + ``getDatabaseName()``, ``isSchemaKeyword()`` and ``isIfNotExists()``. ``USER`` uses + ``getAccount()``, a ``UserAccount`` without authentication; ``isCurrentUser()`` and + ``isCurrentUserParentheses()`` describe ``CURRENT_USER`` and ``CURRENT_USER()``. + +.. code-block:: java + + ShowColumnsStatement columns = (ShowColumnsStatement) CCJSqlParserUtil.parse( + "SHOW FULL COLUMNS FROM orders FROM shop LIKE 'c%'"); + columns.getTableName(); // orders + columns.getDbName(); // shop + + ShowCreateStatement create = (ShowCreateStatement) CCJSqlParserUtil.parse( + "SHOW CREATE USER 'app'@'%'"); + create.getAccount().getHost().getNotExcapedValue(); // % + +A typed statement is used only when it covers the whole statement. Other dialects' +``SHOW`` forms, such as ``SHOW COLUMNS IN SCHEMA s`` or ``SHOW CREATE TABLE t FORMAT f``, +remain ``UnsupportedStatement``. + +Table visitors report the table of ``SHOW COLUMNS`` (qualified by the ``FROM db`` clause) +and the table or view of ``SHOW CREATE TABLE`` and ``SHOW CREATE VIEW``; until now +``SHOW COLUMNS`` raised ``UnsupportedOperationException``. ``SHOW TABLE STATUS`` names no +table. Validators report the ``showColumns``, ``showTableStatus`` and ``showCreate`` +features, which the MySQL and MariaDB capabilities enable. Feature analysis reports +``RETURNS_RESULT_SET``. diff --git a/src/test/java/net/sf/jsqlparser/benchmark/DynamicParserRunner.java b/src/test/java/net/sf/jsqlparser/benchmark/DynamicParserRunner.java index f87acd1195..a9915f994c 100644 --- a/src/test/java/net/sf/jsqlparser/benchmark/DynamicParserRunner.java +++ b/src/test/java/net/sf/jsqlparser/benchmark/DynamicParserRunner.java @@ -9,32 +9,56 @@ */ package net.sf.jsqlparser.benchmark; -import net.sf.jsqlparser.parser.CCJSqlParser; -import net.sf.jsqlparser.statement.Statements; - +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URLClassLoader; +import java.util.EnumMap; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; +/** Owns the isolated class loader and keeps its AST types behind an Object boundary. */ public class DynamicParserRunner implements SqlParserRunner { + private final URLClassLoader loader; private final Method parseStatementsMethod; + private final Map> configurations = + new EnumMap<>(Configuration.class); public DynamicParserRunner(URLClassLoader loader) throws Exception { + this.loader = loader; Class utilClass = loader.loadClass("net.sf.jsqlparser.parser.CCJSqlParserUtil"); - Class ccjClass = loader.loadClass("net.sf.jsqlparser.parser.CCJSqlParser"); - Class consumerClass = Class.forName("java.util.function.Consumer"); // interface OK - parseStatementsMethod = utilClass.getMethod( - "parseStatements", - String.class, - ExecutorService.class, - consumerClass); + Class parserClass = loader.loadClass("net.sf.jsqlparser.parser.CCJSqlParser"); + parseStatementsMethod = utilClass.getMethod("parseStatements", String.class, + ExecutorService.class, Consumer.class); + for (Configuration configuration : Configuration.values()) { + Method method = parserClass.getMethod(configuration.methodName, boolean.class); + configurations.put(configuration, parser -> { + try { + method.invoke(parser, configuration.value); + } catch (ReflectiveOperationException ex) { + throw new IllegalStateException( + "Cannot apply parser configuration " + configuration, ex); + } + }); + } + } + + @Override + public Object parseStatements(String sql, ExecutorService executorService, + Configuration configuration) throws Exception { + try { + return parseStatementsMethod.invoke(null, sql, executorService, + configuration == null ? null : configurations.get(configuration)); + } catch (InvocationTargetException ex) { + if (ex.getCause() instanceof Exception) { + throw (Exception) ex.getCause(); + } + throw ex; + } } @Override - public Statements parseStatements(String sql, - ExecutorService executorService, - Consumer consumer) throws Exception { - return (Statements) parseStatementsMethod.invoke(null, sql, executorService, null); + public void close() throws java.io.IOException { + loader.close(); } } diff --git a/src/test/java/net/sf/jsqlparser/benchmark/JSQLParserBenchmark.java b/src/test/java/net/sf/jsqlparser/benchmark/JSQLParserBenchmark.java index abe61a8048..3786eace5f 100644 --- a/src/test/java/net/sf/jsqlparser/benchmark/JSQLParserBenchmark.java +++ b/src/test/java/net/sf/jsqlparser/benchmark/JSQLParserBenchmark.java @@ -9,8 +9,6 @@ */ package net.sf.jsqlparser.benchmark; -import net.sf.jsqlparser.parser.CCJSqlParser; -import net.sf.jsqlparser.statement.Statements; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; @@ -21,7 +19,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.concurrent.*; -import java.util.function.Consumer; @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @@ -34,7 +31,7 @@ public class JSQLParserBenchmark { SqlParserRunner runner; // @Param({ "latest", "5.2", "5.1", "5.0", "4.9", "4.8", "4.7", "4.6", "4.5" }) - @Param({"latest", "5.3", "5.1"}) + @Param({"latest", "5.4"}) public String version; @Setup(Level.Trial) @@ -44,13 +41,33 @@ public void setup() throws Exception { } else { Path jarPath = downloadJsqlparserJar(version); URLClassLoader loader = new URLClassLoader(new URL[] {jarPath.toUri().toURL()}, null); - runner = new DynamicParserRunner(loader); + try { + runner = new DynamicParserRunner(loader); + } catch (Exception ex) { + loader.close(); + throw ex; + } } - // Adjust path as necessary based on where source root is during test execution - Path path = Paths.get("src/test/resources/net/sf/jsqlparser/performance.sql"); - sqlContent = Files.readString(path, StandardCharsets.UTF_8); - executorService = Executors.newSingleThreadExecutor(); + // Reject incompatible SQL/configurations before collecting measurements. + try { + Path path = Paths.get("src/test/resources/net/sf/jsqlparser/performance.sql"); + sqlContent = Files.readString(path, StandardCharsets.UTF_8); + executorService = Executors.newSingleThreadExecutor(); + Object statements = runner.parseStatements(sqlContent, executorService, + SqlParserRunner.Configuration.SIMPLE); + if (statements == null) { + throw new IllegalStateException("Parser " + version + + " returned no statements for the benchmark corpus with SIMPLE configuration"); + } + } catch (Exception ex) { + try { + tearDown(); + } catch (Exception cleanup) { + ex.addSuppressed(cleanup); + } + throw ex; + } } private Path downloadJsqlparserJar(String version) throws IOException { @@ -74,10 +91,10 @@ private Path downloadJsqlparserJar(String version) throws IOException { @Benchmark public void parseSQLStatements(Blackhole blackhole) throws Exception { - final Statements statements = runner.parseStatements( + final Object statements = runner.parseStatements( sqlContent, executorService, - (Consumer) parser -> parser.withAllowComplexParsing(false)); + SqlParserRunner.Configuration.SIMPLE); blackhole.consume(statements); } @@ -87,15 +104,23 @@ public void parseQuotedText(Blackhole blackhole) throws Exception { + "INSERT INTO recycle_record (a,f) VALUES ('\\'anything', 'abc');\n" + "INSERT INTO recycle_record (a,f) VALUES ('\\'','83653692186728700711687663398101');\n"; - final Statements statements = runner.parseStatements( + final Object statements = runner.parseStatements( sqlStr, executorService, - (Consumer) parser -> parser.withBackslashEscapeCharacter(true)); + SqlParserRunner.Configuration.BACKSLASH_ESCAPES); blackhole.consume(statements); } @TearDown(Level.Trial) - public void tearDown() { - executorService.shutdown(); + public void tearDown() throws Exception { + try { + if (executorService != null) { + executorService.shutdownNow(); + } + } finally { + if (runner != null) { + runner.close(); + } + } } } diff --git a/src/test/java/net/sf/jsqlparser/benchmark/LatestClasspathRunner.java b/src/test/java/net/sf/jsqlparser/benchmark/LatestClasspathRunner.java index 5f70cf878f..5083716f3c 100644 --- a/src/test/java/net/sf/jsqlparser/benchmark/LatestClasspathRunner.java +++ b/src/test/java/net/sf/jsqlparser/benchmark/LatestClasspathRunner.java @@ -9,20 +9,17 @@ */ package net.sf.jsqlparser.benchmark; -import net.sf.jsqlparser.parser.CCJSqlParser; -import net.sf.jsqlparser.statement.Statements; import java.util.concurrent.ExecutorService; -import java.util.function.Consumer; public class LatestClasspathRunner implements SqlParserRunner { @Override - public Statements parseStatements(String sql, + public Object parseStatements(String sql, ExecutorService executorService, - Consumer consumer) throws Exception { + Configuration configuration) throws Exception { return net.sf.jsqlparser.parser.CCJSqlParserUtil.parseStatements(sql, executorService, - consumer); + configuration == null ? null : configuration.currentParserConfiguration); } } diff --git a/src/test/java/net/sf/jsqlparser/benchmark/ParserRunnerTest.java b/src/test/java/net/sf/jsqlparser/benchmark/ParserRunnerTest.java new file mode 100644 index 0000000000..f0b04f2015 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/benchmark/ParserRunnerTest.java @@ -0,0 +1,47 @@ +/*- + * #%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.benchmark; + +import static org.junit.jupiter.api.Assertions.*; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.concurrent.Executors; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import org.junit.jupiter.api.Test; +import net.sf.jsqlparser.benchmark.SqlParserRunner.Configuration; + +class ParserRunnerTest { + @Test + void isolatedParserReturnsItsOwnAstAndAppliesTheSameOptions() throws Exception { + URL classes = CCJSqlParserUtil.class.getProtectionDomain().getCodeSource().getLocation(); + var executor = Executors.newSingleThreadExecutor(); + try (var current = new LatestClasspathRunner(); + var isolated = + new DynamicParserRunner(new URLClassLoader(new URL[] {classes}, null))) { + String sql = "SELECT 'it\\'s' AS value"; + Object expected = + current.parseStatements(sql, executor, Configuration.BACKSLASH_ESCAPES); + Object actual = + isolated.parseStatements(sql, executor, Configuration.BACKSLASH_ESCAPES); + assertEquals(expected.toString(), actual.toString()); + assertNotSame(expected.getClass(), actual.getClass()); + assertEquals(expected.getClass().getName(), actual.getClass().getName()); + assertEquals( + current.parseStatements("SELECT 1", executor, Configuration.SIMPLE).toString(), + isolated.parseStatements("SELECT 1", executor, Configuration.SIMPLE) + .toString()); + assertThrows(Exception.class, + () -> isolated.parseStatements("SELECT FROM", executor, Configuration.SIMPLE)); + assertFalse(executor.isShutdown()); + } finally { + executor.shutdownNow(); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/benchmark/SqlParserRunner.java b/src/test/java/net/sf/jsqlparser/benchmark/SqlParserRunner.java index 00496ad685..1481d040c9 100644 --- a/src/test/java/net/sf/jsqlparser/benchmark/SqlParserRunner.java +++ b/src/test/java/net/sf/jsqlparser/benchmark/SqlParserRunner.java @@ -9,13 +9,31 @@ */ package net.sf.jsqlparser.benchmark; -import net.sf.jsqlparser.parser.CCJSqlParser; -import net.sf.jsqlparser.statement.Statements; - import java.util.concurrent.ExecutorService; import java.util.function.Consumer; +import net.sf.jsqlparser.parser.CCJSqlParser; + +public interface SqlParserRunner extends AutoCloseable { + enum Configuration { + SIMPLE("withAllowComplexParsing", false, + parser -> parser.withAllowComplexParsing(false)), BACKSLASH_ESCAPES( + "withBackslashEscapeCharacter", true, + parser -> parser.withBackslashEscapeCharacter(true)); + + final String methodName; + final boolean value; + final Consumer currentParserConfiguration; + + Configuration(String methodName, boolean value, Consumer configuration) { + this.methodName = methodName; + this.value = value; + this.currentParserConfiguration = configuration; + } + } + + Object parseStatements(String sql, ExecutorService executorService, + Configuration configuration) throws Exception; -public interface SqlParserRunner { - Statements parseStatements(String sql, ExecutorService executorService, - Consumer consumer) throws Exception; + @Override + default void close() throws Exception {} } diff --git a/src/test/java/net/sf/jsqlparser/expression/AnalyticExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/AnalyticExpressionTest.java index df4ef1eba5..b1a7b059f9 100644 --- a/src/test/java/net/sf/jsqlparser/expression/AnalyticExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/AnalyticExpressionTest.java @@ -43,4 +43,23 @@ void testDatabricks() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testPartitionByBracketedOperand() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT SUM(x) OVER (PARTITION BY (1 + a) / 2 ORDER BY b) FROM t", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT SUM(x) OVER w FROM t WINDOW w AS (PARTITION BY (1 + a) / 2)", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT LISTAGG(x, ',') WITHIN GROUP (ORDER BY y) OVER (PARTITION BY (1 + a) / 2) FROM t", + true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t PREFERRING HIGH x PARTITION BY (1 + a) / 2", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT SUM(x) OVER (PARTITION BY (a, b), c) FROM t", true); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/ArrayConstructorTest.java b/src/test/java/net/sf/jsqlparser/expression/ArrayConstructorTest.java new file mode 100644 index 0000000000..10e83537a3 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/ArrayConstructorTest.java @@ -0,0 +1,110 @@ +/*- + * #%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 static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ArrayConstructorTest { + @ParameterizedTest + @ValueSource(strings = {"ARRAY['a', (1 + 0)::text]", "ARRAY[(1 + 2), 3]", + "ARRAY[1, (2 + 3), 4]", "ARRAY[1, ((2 * 3) + 4)]", + "ARRAY[(SELECT 1), 2]", "ARRAY[NULL, COALESCE(NULL, (2 + 3))]", + "ARRAY[CASE WHEN 1 < 2 THEN (3 + 4) ELSE 5 END, 6]", + "ARRAY[ARRAY[(1 + 2), 3], ARRAY[4, (5 + 6)]]", + "ARRAY[[(1 + 2), 3], [4, (5 + 6)]]", "ARRAY[]::integer[]", + "ARRAY[ROW(1, 2), ROW(3, 4)]", "ARRAY[(1 < 2), (3 > 4)]"}) + void parsesGeneralExpressionsAtEveryArrayPosition(String expression) throws Exception { + for (boolean complex : List.of(false, true)) { + Statement statement = CCJSqlParserUtil.parse("SELECT " + expression, + p -> p.withDialect(Dialect.POSTGRESQL).withAllowComplexParsing(complex)); + roundTrip(statement); + } + } + + @Test + void retainsCastAndParenthesesInTheElementAst() throws Exception { + PlainSelect select = (PlainSelect) parse("SELECT ARRAY['a', (1 + 0)::text]"); + ArrayConstructor array = assertInstanceOf(ArrayConstructor.class, + select.getSelectItem(0).getExpression()); + assertEquals(2, array.getExpressions().size()); + CastExpression cast = assertInstanceOf(CastExpression.class, array.getExpressions().get(1)); + assertInstanceOf(ParenthesedExpressionList.class, cast.getLeftExpression()); + cast.setLeftExpression(new LongValue(42)); + assertEquals("SELECT ARRAY['a', 42::text]", select.toString()); + roundTrip(select); + } + + @Test + void visitsElementsAndFindsTablesInScalarSubqueries() throws Exception { + Statement statement = parse("SELECT ARRAY[1, (SELECT value FROM hidden), (2 + 3)]"); + assertThat(new TablesNamesFinder().getTables(statement)).containsExactly("hidden"); + List seen = new ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + seen.add(value.getValue()); + return getBuilder().append(value.getValue() + 10); + } + }; + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(List.of(1L, 2L, 3L), seen); + assertEquals("SELECT ARRAY[11, (SELECT value FROM hidden), (12 + 13)]", output.toString()); + roundTrip(parse(output.toString())); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT ARRAY[1,]", "SELECT ARRAY[,1]", "SELECT ARRAY[(1 +)]", + "SELECT ARRAY[1, (2 + 3]", "SELECT ARRAY[[1,2],]"}) + void rejectsIncompleteElements(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT [1, (2 + 3)]", "SELECT [(1 + 2), 3]", + "SELECT ARRAY[1, (2 + 3)]", + "SELECT ARRAY[1:3]", "SELECT a[(1 + 2)] FROM t", + "SELECT a.b[1:2].c FROM t", "SELECT ARRAY[ARRAY[], ARRAY[]]"}) + void preservesExistingArrayAndSubscriptForms(String sql) throws Exception { + Statement statement = CCJSqlParserUtil.parse(sql); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws Exception { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/ArrayExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/ArrayExpressionTest.java index dd96441004..90ad192fca 100644 --- a/src/test/java/net/sf/jsqlparser/expression/ArrayExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/ArrayExpressionTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.SelectItem; @@ -30,4 +31,173 @@ void testColumnArrayExpression() throws JSQLParserException { assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); } + @Test + void testNavigationAfterSubscript() throws JSQLParserException { + // Redshift SUPER navigation: subscript then field access + String sqlStr = "SELECT recommendations[0].language_id FROM recs"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + SelectItem selectItem = select.getSelectItem(0); + + RowGetExpression rowGet = selectItem.getExpression(RowGetExpression.class); + assertNotNull(rowGet); + assertEquals("language_id", rowGet.getColumnName()); + Column column = assertInstanceOf(Column.class, rowGet.getExpression()); + assertEquals("recommendations", column.getColumnName()); + assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); + } + + @Test + void testAlternatingNavigationChain() throws JSQLParserException { + // field accesses and subscripts may alternate freely after the primary expression + String sqlStr = "SELECT a.b[0].c[1].d FROM t"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + SelectItem selectItem = select.getSelectItem(0); + + RowGetExpression outer = selectItem.getExpression(RowGetExpression.class); + assertNotNull(outer); + assertEquals("d", outer.getColumnName()); + ArrayExpression subscript = assertInstanceOf(ArrayExpression.class, outer.getExpression()); + assertInstanceOf(LongValue.class, subscript.getIndexExpression()); + RowGetExpression inner = + assertInstanceOf(RowGetExpression.class, subscript.getObjExpression()); + assertEquals("c", inner.getColumnName()); + Column column = assertInstanceOf(Column.class, inner.getExpression()); + assertEquals("a", column.getTable().getName()); + assertEquals("b", column.getColumnName()); + assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); + } + + @Test + void testNavigationFieldNames() throws JSQLParserException { + // dotted continuations accept FROM/SELECT/CURRENT and quoted names, + // same as ColumnIdentifier's dotted continuation (RelObjectNameExt) + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].from FROM t", true); + RowGetExpression rowGet = select.getSelectItem(0).getExpression(RowGetExpression.class); + assertEquals("from", rowGet.getColumnName()); + + select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].\"quoted field\" FROM t", true); + rowGet = select.getSelectItem(0).getExpression(RowGetExpression.class); + assertEquals("\"quoted field\"", rowGet.getColumnName()); + } + + @Test + void testNavigationInClauses() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE a[0].b = 1", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t ORDER BY a[0].b", true); + } + + @Test + void testNavigationThenCastAndJson() throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].b::text FROM t", true); + CastExpression cast = select.getSelectItem(0).getExpression(CastExpression.class); + assertInstanceOf(RowGetExpression.class, cast.getLeftExpression()); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].b -> 'x' FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.b[1 : 2].c FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].b[1][2].c FROM t", true); + } + + @Test + void testNavigationDoesNotSwallowQualifiedColumns() throws JSQLParserException { + // the navigation loop must not change how plain qualified columns parse + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.b FROM t", true); + Column column = select.getSelectItem(0).getExpression(Column.class); + assertEquals("a", column.getTable().getName()); + assertEquals("b", column.getColumnName()); + assertNull(column.getArrayConstructor()); + + select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.b.c FROM t", true); + column = select.getSelectItem(0).getExpression(Column.class); + assertEquals("a.b.c", column.toString()); + + select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.b[0] FROM t", true); + column = select.getSelectItem(0).getExpression(Column.class); + assertEquals("a", column.getTable().getName()); + assertEquals("b", column.getColumnName()); + assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); + + // a bracket group after a subscript stays on the ArrayExpression slot, + // not on the navigation loop + select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0]['k'] FROM t", true); + ArrayExpression array = select.getSelectItem(0).getExpression(ArrayExpression.class); + assertNotNull(array); + column = assertInstanceOf(Column.class, array.getObjExpression()); + assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); + } + + @Test + void testMalformedNavigationRejected() { + assertThrows(JSQLParserException.class, + () -> TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a[0].* FROM t", true)); + } + + @Test + void testColumnSlices() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT l[2:3] FROM t", "SELECT l[:2] FROM t", + "SELECT l[2:] FROM t", "SELECT l[:] FROM t", "SELECT l[-5:] FROM t", + "SELECT l[1:-1:2] FROM t", "SELECT l[1:-:2] FROM t", "SELECT l[:-:-1] FROM t", + "SELECT l[x:y] FROM t", "SELECT s[2:] > 'a' FROM t"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + PlainSelect select = (PlainSelect) TestUtils + .assertSqlCanBeParsedAndDeparsed("SELECT l[1:-:2] FROM t", true); + RangeExpression slice = (RangeExpression) select.getSelectItem(0) + .getExpression(Column.class).getArrayConstructor().getExpressions().get(0); + assertEquals("1", slice.getStartExpression().toString()); + assertNull(slice.getEndExpression()); + assertEquals("2", slice.getStepExpression().toString()); + } + + @Test + void testExpressionSlices() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT f()[2:] FROM t", "SELECT (l)[:2] FROM t", + "SELECT ([1, 2, 3])[1:-:-1]", "SELECT l[1][2:3] FROM t", + "SELECT l[:2][1] FROM t"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + ArrayExpression slice = (ArrayExpression) CCJSqlParserUtil.parseExpression("f()[x:y:2]"); + assertNull(slice.getIndexExpression()); + assertEquals("x", slice.getStartIndexExpression().toString()); + assertEquals("y", slice.getStopIndexExpression().toString()); + assertEquals("2", slice.getStepExpression().toString()); + } + + @Test + void testEmptyStepIsDropped() throws JSQLParserException { + assertEquals("l[1:2]", CCJSqlParserUtil.parseExpression("l[1:2:]").toString()); + } + + @Test + void testJsonPathOutsideSubscripts() throws JSQLParserException { + assertInstanceOf(JsonExpression.class, CCJSqlParserUtil.parseExpression("c:a[1]")); + } + + @Test + void testJsonPathNestedInSubscript() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT l[(SELECT c : a FROM t2)] FROM t", + "SELECT l[f(x, c : a)] FROM t", "SELECT l[CASE WHEN c : a = 1 THEN 1 END] FROM t", + "SELECT f()[(SELECT c : a FROM t)]"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + PlainSelect select = (PlainSelect) TestUtils + .assertSqlCanBeParsedAndDeparsed("SELECT l[ARRAY[c : a]] FROM t", true); + ArrayConstructor array = (ArrayConstructor) select.getSelectItem(0) + .getExpression(Column.class).getArrayConstructor().getExpressions().get(0); + assertInstanceOf(JsonExpression.class, array.getExpressions().get(0)); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java index a14d5c9a00..afb6778ade 100644 --- a/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java @@ -13,8 +13,10 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.StatementDeParser; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -24,6 +26,27 @@ */ public class CastExpressionTest { + @Test + void testCastToSingleFieldRow() throws JSQLParserException { + String sql = "SELECT CAST(ROW(1) AS ROW(x BIGINT))"; + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql); + CastExpression cast = Assertions.assertInstanceOf(CastExpression.class, + select.getSelectItem(0).getExpression()); + Assertions.assertEquals(1, cast.getColumnDefinitions().size()); + Assertions.assertEquals("x", cast.getColumnDefinitions().get(0).getColumnName()); + Assertions.assertEquals("BIGINT", + cast.getColumnDefinitions().get(0).getColDataType().getDataType()); + + Assertions.assertAll( + () -> assertSqlCanBeParsedAndDeparsed(sql), + () -> { + StringBuilder builder = new StringBuilder(); + select.accept(new StatementDeParser(builder)); + Assertions.assertEquals(sql, builder.toString()); + }); + assertSqlCanBeParsedAndDeparsed("SELECT TRY_CAST(ROW(1) AS ROW(x BIGINT))"); + } + @Test public void testCastToRowConstructorIssue1267() throws JSQLParserException { TestUtils.assertExpressionCanBeParsedAndDeparsed( @@ -80,6 +103,21 @@ void testImplicitCastDoublePrecisionIssue1344() throws JSQLParserException { Assertions.assertTrue(select.getSelectItem(0).getExpression() instanceof CastExpression); } + @Test + void testCastCharWithCharacterSet() throws JSQLParserException { + String sqlStr = "SELECT CAST('abc' AS CHAR CHARACTER SET utf8mb4)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sqlStr, true); + CastExpression castExpression = Assertions.assertInstanceOf(CastExpression.class, + select.getSelectItem(0).getExpression()); + + Assertions.assertEquals("CHAR", castExpression.getColDataType().getDataType()); + Assertions.assertEquals("utf8mb4", castExpression.getColDataType().getCharacterSet()); + + assertSqlCanBeParsedAndDeparsed( + "SELECT CAST('abc' AS CHAR(10) CHARACTER SET utf8mb4)", true); + assertSqlCanBeParsedAndDeparsed("SELECT CAST('abc' AS CHARACTER VARYING)", true); + } + @Test public void testCastToSigned() throws JSQLParserException { @@ -152,4 +190,18 @@ void testNestedCompositeTypeCastIssue2341() throws JSQLParserException { Assertions.assertEquals(1, parenthesedCast.size()); Assertions.assertInstanceOf(CastExpression.class, parenthesedCast.get(0)); } + + @Test + public void testCastToArrayIssue2490() throws JSQLParserException { + // MySQL casts to an array of the given type when a multi-valued index key part is + // defined, and accepts the same cast anywhere else an expression is allowed. + assertSqlCanBeParsedAndDeparsed("SELECT CAST(data -> '$.zips' AS UNSIGNED ARRAY) FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT CAST(x AS CHAR (10) ARRAY) FROM t"); + + PlainSelect select = (PlainSelect) CCJSqlParserUtil + .parse("SELECT CAST(data -> '$.zips' AS UNSIGNED ARRAY) FROM t"); + CastExpression cast = Assertions.assertInstanceOf(CastExpression.class, + select.getSelectItem(0).getExpression()); + Assertions.assertEquals("UNSIGNED ARRAY", cast.getColDataType().getDataType()); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/ColumnsExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/ColumnsExpressionTest.java new file mode 100644 index 0000000000..bdfa302ef6 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/ColumnsExpressionTest.java @@ -0,0 +1,201 @@ +/*- + * #%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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.validator.ExpressionValidator; +import org.junit.jupiter.api.Test; + +class ColumnsExpressionTest { + + private static final String SQL = "SELECT COLUMNS('m') APPLY(x -> x + 1)" + + " EXCEPT (skipped) REPLACE(value + 2 AS value) FROM metrics"; + + @Test + void testDeparserVisitsTransformerChildren() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(SQL); + StringBuilder buffer = new StringBuilder(); + List columns = new ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + return getBuilder().append("'renamed'"); + } + + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append('?'); + } + + @Override + public StringBuilder visit(Column column, S context) { + columns.add(column.getColumnName()); + return super.visit(column, context); + } + }; + select.accept(new StatementDeParser(expressions, new SelectDeParser(), buffer), null); + + assertEquals("SELECT COLUMNS('renamed') APPLY(x -> x + ?)" + + " EXCEPT (skipped) REPLACE(value + ? AS value) FROM metrics", buffer.toString()); + assertEquals(List.of("x", "skipped", "value"), columns); + assertEquals(SQL, select.toString()); + assertSqlCanBeParsedAndDeparsed(SQL, true); + } + + @Test + void testAdapterVisitsEveryTransformerInOrder() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(SQL); + ColumnsExpression expression = assertInstanceOf(ColumnsExpression.class, + select.getSelectItems().get(0).getExpression()); + List visited = new ArrayList<>(); + Object marker = new Object(); + expression.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + assertSame(marker, context); + visited.add(value.getValue()); + return null; + } + + @Override + public Void visit(Column column, S context) { + assertSame(marker, context); + visited.add(column.getColumnName()); + return null; + } + }, marker); + assertEquals(List.of("m", "x", "skipped", "value"), visited); + } + + @Test + void testAdapterPreservesCollectedResult() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT COLUMNS('m') EXCEPT (skipped) FROM metrics"); + Expression expression = select.getSelectItems().get(0).getExpression(); + ExpressionVisitorAdapter> collector = new ExpressionVisitorAdapter<>() { + @Override + protected List applyExpression(Expression value, S context) { + return List.of(value.toString()); + } + + @Override + protected List visitExpressions(Expression value, S context, + Collection children) { + List result = new ArrayList<>(); + for (Expression child : children) { + if (child != null) { + result.addAll(child.accept(this, context)); + } + } + return result; + } + }; + assertEquals(List.of("'m'", "skipped"), expression.accept(collector, null)); + } + + @Test + void testVisitorDefaultDispatchesChildren() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(SQL); + ColumnsExpression expression = assertInstanceOf(ColumnsExpression.class, + select.getSelectItems().get(0).getExpression()); + ExpressionVisitor visitor = mock(ExpressionVisitor.class, CALLS_REAL_METHODS); + Object context = new Object(); + expression.accept(visitor, context); + + verify(visitor).visit((Function) expression.getColumns(), context); + verify(visitor).visit((LambdaExpression) expression.getTransformers().get(0) + .getApplyExpression(), context); + verify(visitor).visit(expression.getTransformers().get(1).getExceptColumns().get(0), + context); + verify(visitor).visit( + (Addition) expression + .getTransformers().get(2).getReplaceItems().get(0).getExpression(), + context); + } + + @Test + void testValidatorVisitsExceptColumns() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(SQL); + ColumnsExpression expression = assertInstanceOf(ColumnsExpression.class, + select.getSelectItems().get(0).getExpression()); + List visited = new ArrayList<>(); + ExpressionValidator validator = new ExpressionValidator() { + @Override + public Void visit(Column column, S context) { + visited.add(column.getColumnName()); + return null; + } + }; + validator.setContext(new ValidationContext().setCapabilities(Collections.emptyList())); + expression.accept(validator, null); + assertEquals(List.of("x", "skipped", "value"), visited); + } + + @Test + void testFinderVisitsExceptColumns() throws JSQLParserException { + List visited = new ArrayList<>(); + TablesNamesFinder finder = new TablesNamesFinder<>() { + @Override + public Void visit(Column column, S context) { + visited.add(column.getColumnName()); + return super.visit(column, context); + } + }; + assertEquals(Set.of("metrics"), finder.getTables(CCJSqlParserUtil.parse(SQL))); + assertEquals(List.of("x", "skipped", "value"), visited); + } + + @Test + void testAstTokenRangeIncludesMatcher() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT COLUMNS('m') APPLY(sum) AS result FROM metrics"); + ColumnsExpression expression = assertInstanceOf(ColumnsExpression.class, + select.getSelectItem(0).getExpression()); + assertEquals("COLUMNS", expression.getASTNode().jjtGetFirstToken().image); + assertEquals(")", expression.getASTNode().jjtGetLastToken().image); + assertEquals("AS", expression.getASTNode().jjtGetLastToken().next.image); + assertEquals("COLUMNS", expression.getColumns().getASTNode().jjtGetFirstToken().image); + assertEquals("APPLY", expression.getColumns().getASTNode().jjtGetLastToken().next.image); + assertSame(expression, ((Function) expression.getColumns()).getParent()); + assertSame(expression, ((Function) expression.getColumns()) + .getParent(ColumnsExpression.class)); + } + + @Test + void testFinderVisitsTransformerSubqueries() throws JSQLParserException { + String sql = "SELECT COLUMNS('m') APPLY(x -> (SELECT max(v) FROM applied))" + + " REPLACE((SELECT v FROM replacement) AS c) FROM metrics"; + assertEquals(Set.of("metrics", "applied", "replacement"), + TablesNamesFinder.findTables(sql)); + assertSqlCanBeParsedAndDeparsed(sql, true); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/ColumnsTransformerTest.java b/src/test/java/net/sf/jsqlparser/expression/ColumnsTransformerTest.java new file mode 100644 index 0000000000..44efd7e81d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/ColumnsTransformerTest.java @@ -0,0 +1,112 @@ +/*- + * #%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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.AllColumns; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ColumnsTransformerTest { + + @ParameterizedTest + @ValueSource(strings = {"*", "t.*", "COLUMNS('m')"}) + void testExceptColumnsReplacePattern(String matcher) throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT " + matcher + " EXCEPT STRICT '^tmp_' FROM t"); + Expression expression = select.getSelectItem(0).getExpression(); + ColumnsTransformer transformer = getTransformer(expression); + transformer.setExceptColumns(new ParenthesedExpressionList<>(new Column("keep"))); + + assertOutput("SELECT " + matcher + " EXCEPT STRICT (keep) FROM t", select); + assertNull(transformer.getExceptPattern()); + if (expression instanceof ColumnsExpression) { + assertVisitedValues(List.of("m", "keep"), expression); + } + } + + @ParameterizedTest + @ValueSource(strings = {"*", "t.*", "COLUMNS('m')"}) + void testExceptPatternReplacesColumns(String matcher) throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT " + matcher + " EXCEPT STRICT (keep) FROM t"); + Expression expression = select.getSelectItem(0).getExpression(); + ColumnsTransformer transformer = getTransformer(expression); + transformer.setExceptPattern(new StringValue("'^tmp_'")); + + assertOutput("SELECT " + matcher + " EXCEPT STRICT '^tmp_' FROM t", select); + if (expression instanceof ColumnsExpression) { + assertVisitedValues(List.of("m", "^tmp_"), expression); + } + assertNull(transformer.getExceptColumns()); + } + + @Test + void testClearingPatternPreservesColumns() { + ColumnsTransformer transformer = + new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCEPT) + .setExceptColumns(new ParenthesedExpressionList<>(new Column("keep"))) + .setExceptPattern(null); + assertEquals("EXCEPT (keep)", transformer.toString()); + } + + @Test + void testClearingColumnsPreservesPattern() { + ColumnsTransformer transformer = + new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCEPT) + .setExceptPattern(new StringValue("'^tmp_'")) + .setExceptColumns(null); + assertEquals("EXCEPT '^tmp_'", transformer.toString()); + } + + private static ColumnsTransformer getTransformer(Expression expression) { + return expression instanceof AllColumns + ? ((AllColumns) expression).getTransformers().get(0) + : ((ColumnsExpression) expression).getTransformers().get(0); + } + + private static void assertOutput(String expected, PlainSelect select) + throws JSQLParserException { + assertEquals(expected, select.toString()); + StatementDeParser deparser = new StatementDeParser(new StringBuilder()); + select.accept(deparser, null); + assertEquals(expected, deparser.getBuilder().toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected).toString()); + } + + private static void assertVisitedValues(List expected, Expression expression) { + List visited = new ArrayList<>(); + expression.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + visited.add(value.getValue()); + return null; + } + + @Override + public Void visit(Column column, S context) { + visited.add(column.getColumnName()); + return null; + } + }, null); + assertEquals(expected, visited); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java index 164c9e112a..920070b8f1 100644 --- a/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java @@ -10,7 +10,10 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.test.TestUtils; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -27,4 +30,67 @@ void testParsing() throws JSQLParserException { Function f = select.getSelectItem(0).getExpression(Function.class); Assertions.assertThat(f.getParameters().get(1)).isInstanceOf(DateUnitExpression.class); } + + @Test + void testUnitArgumentKeepsItsSpelling() throws JSQLParserException { + String sqlStr = "SELECT DATEADD(day, 1, d) FROM t"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Function f = select.getSelectItem(0).getExpression(Function.class); + DateUnitExpression unit = (DateUnitExpression) f.getParameters().get(0); + Assertions.assertThat(unit.getType()).isEqualTo(DateUnitExpression.DateUnit.DAY); + Assertions.assertThat(unit).hasToString("day"); + } + + @Test + void testUnitWordOutsideFunctionArgumentsIsColumn() throws JSQLParserException { + String sqlStr = "SELECT year FROM t"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Column column = select.getSelectItem(0).getExpression(Column.class); + Assertions.assertThat(column.getColumnName()).isEqualTo("year"); + } + + @Test + void testUnitWordAsQualifierIsColumn() throws JSQLParserException { + String sqlStr = "SELECT month.amount FROM sales month"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Column column = select.getSelectItem(0).getExpression(Column.class); + Assertions.assertThat(column.getFullyQualifiedName()).isEqualTo("month.amount"); + } + + @Test + void testUnitWordWithinArgumentExpressionIsColumn() throws JSQLParserException { + String sqlStr = "SELECT COALESCE(year + 1, 0) FROM t"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Function f = select.getSelectItem(0).getExpression(Function.class); + Addition addition = (Addition) f.getParameters().get(0); + Assertions.assertThat(addition.getLeftExpression()).isInstanceOf(Column.class); + } + + @Test + void testQuotedUnitWordArgumentIsColumn() throws JSQLParserException { + String sqlStr = "SELECT DATEADD(\"day\", 1, d) FROM t"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Function f = select.getSelectItem(0).getExpression(Function.class); + Assertions.assertThat(f.getParameters().get(0)).isInstanceOf(Column.class); + } + + @Test + void testUnitWordInUpdateSetTupleIsColumn() throws JSQLParserException { + String sqlStr = "UPDATE t SET (a, year) = (1, 2)"; + + Update update = (Update) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Assertions.assertThat(update.getUpdateSets().get(0).getColumn(1).getColumnName()) + .isEqualTo("year"); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/DigitSeparatorsTest.java b/src/test/java/net/sf/jsqlparser/expression/DigitSeparatorsTest.java new file mode 100644 index 0000000000..c9b46ac602 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/DigitSeparatorsTest.java @@ -0,0 +1,64 @@ +/*- + * #%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 net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +class DigitSeparatorsTest { + + private static Expression firstItem(String sql, boolean digitSeparators) + throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql, + parser -> parser.withDigitSeparators(digitSeparators)); + return select.getSelectItem(0).getExpression(); + } + + @ParameterizedTest + @CsvSource({"1_000, 1000", "1_000_000, 1000000", "92_000.0, 92000.0", "1_000.5_5, 1000.55", + ".5_5, .55", "1e1_0, 1e10", "1_0e10, 10e10", "1.5e+1_0, 1.5e+10"}) + void testSeparatorsAreDropped(String literal, String expected) throws JSQLParserException { + assertEquals(expected, firstItem("SELECT " + literal, true).toString()); + } + + @Test + void testSeparatedNumbersAreNumbers() throws JSQLParserException { + assertEquals(1000L, ((LongValue) firstItem("SELECT 1_000", true)).getValue()); + assertEquals(92000.0, ((DoubleValue) firstItem("SELECT 92_000.0", true)).getValue()); + assertEquals("SELECT * FROM t LIMIT 1000", CCJSqlParserUtil + .parse("SELECT * FROM t LIMIT 1_000", parser -> parser.withDigitSeparators()) + .toString()); + } + + @Test + void testPostgreSqlAllowsSeparators() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT 1_000", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertInstanceOf(LongValue.class, select.getSelectItem(0).getExpression()); + } + + // MySQL identifiers may start with digits + @Test + void testWithoutSeparatorsDigitLedNamesStayIdentifiers() throws JSQLParserException { + assertInstanceOf(Column.class, firstItem("SELECT 1_000", false)); + assertInstanceOf(Column.class, firstItem("SELECT 2024_01.c FROM 2024_01", false)); + assertEquals("SELECT .5 _5", CCJSqlParserUtil.parse("SELECT .5_5").toString()); + assertEquals("SELECT 1.5e+1 _0", CCJSqlParserUtil.parse("SELECT 1.5e+1_0").toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java b/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java index a601f6e0ac..040607ddde 100644 --- a/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java @@ -10,9 +10,32 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; +import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; +import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseOr; +import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseRightShift; +import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor; import net.sf.jsqlparser.expression.operators.arithmetic.Concat; +import net.sf.jsqlparser.expression.operators.arithmetic.Division; +import net.sf.jsqlparser.expression.operators.arithmetic.Modulo; +import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication; +import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.conditional.XorExpression; +import net.sf.jsqlparser.expression.operators.relational.Between; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; +import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; +import net.sf.jsqlparser.expression.operators.relational.LikeExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -25,9 +48,301 @@ public class ExpressionPrecedenceTest { @Test public void testGetSign() throws JSQLParserException { + // || binds like +, so tighter than & Expression expr = CCJSqlParserUtil.parseExpression("1&2||3"); + Assertions.assertInstanceOf(BitwiseAnd.class, expr); + Assertions.assertInstanceOf(LongValue.class, ((BitwiseAnd) expr).getLeftExpression()); + Assertions.assertInstanceOf(Concat.class, ((BitwiseAnd) expr).getRightExpression()); + + // PostgreSQL puts || and & on one level + expr = CCJSqlParserUtil.parseExpression("1&2||3", false, + parser -> parser.withDialect(Dialect.POSTGRESQL)); Assertions.assertInstanceOf(Concat.class, expr); Assertions.assertInstanceOf(BitwiseAnd.class, ((Concat) expr).getLeftExpression()); Assertions.assertInstanceOf(LongValue.class, ((Concat) expr).getRightExpression()); } + + @Test + public void testNotNotConditionIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not 1 = 1"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(EqualsTo.class, inner.getExpression()); + Assertions.assertEquals("NOT NOT 1 = 1", expr.toString()); + } + + @Test + public void testNotNotNotConditionIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not not 1 = 1"); + NotExpression first = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression second = Assertions.assertInstanceOf(NotExpression.class, + first.getExpression()); + NotExpression third = Assertions.assertInstanceOf(NotExpression.class, + second.getExpression()); + Assertions.assertInstanceOf(EqualsTo.class, third.getExpression()); + Assertions.assertEquals("NOT NOT NOT 1 = 1", expr.toString()); + } + + @Test + public void testNotNotExistsIssue1170() throws JSQLParserException { + Expression expr = + CCJSqlParserUtil.parseCondExpression("not not exists (select 1 from mytable)"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(ExistsExpression.class, inner.getExpression()); + } + + @Test + public void testNotNotInIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not a in (1, 2)"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(InExpression.class, inner.getExpression()); + } + + @Test + public void testNotNotNotBetweenIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not not a between 1 and 2"); + NotExpression first = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression second = Assertions.assertInstanceOf(NotExpression.class, + first.getExpression()); + NotExpression third = Assertions.assertInstanceOf(NotExpression.class, + second.getExpression()); + Assertions.assertInstanceOf(Between.class, third.getExpression()); + } + + @Test + public void testNotNotNotWhereClauseIssue1170() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE NOT NOT NOT a = 1"); + } + + @Test + public void testMixedNotAndExclamationMarkIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not !1 = 1"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + Assertions.assertFalse(outer.isExclamationMark()); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertTrue(inner.isExclamationMark()); + Assertions.assertInstanceOf(EqualsTo.class, inner.getExpression()); + Assertions.assertEquals("NOT ! 1 = 1", expr.toString()); + } + + @Test + public void testSingleNotConditionStaysFlat() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not 1 = 1"); + NotExpression not = Assertions.assertInstanceOf(NotExpression.class, expr); + Assertions.assertInstanceOf(EqualsTo.class, not.getExpression()); + } + + @Test + public void testNotNotLikeIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not a like 'x'"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + LikeExpression like = Assertions.assertInstanceOf(LikeExpression.class, + inner.getExpression()); + Assertions.assertFalse(like.isNot()); + Assertions.assertInstanceOf(Column.class, like.getLeftExpression()); + } + + @Test + public void testNotNotIsNullIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not a is null"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + IsNullExpression isNull = Assertions.assertInstanceOf(IsNullExpression.class, + inner.getExpression()); + Assertions.assertFalse(isNull.isNot()); + Assertions.assertInstanceOf(Column.class, isNull.getLeftExpression()); + } + + @Test + public void testNotNotSimilarToIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not a similar to 'x'"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + LikeExpression similar = Assertions.assertInstanceOf(LikeExpression.class, + inner.getExpression()); + Assertions.assertFalse(similar.isNot()); + Assertions.assertInstanceOf(Column.class, similar.getLeftExpression()); + } + + @Test + public void testNotNotInsideAndChainIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("a and not not b = 1"); + AndExpression and = Assertions.assertInstanceOf(AndExpression.class, expr); + Assertions.assertInstanceOf(Column.class, and.getLeftExpression()); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, + and.getRightExpression()); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + EqualsTo equalsTo = Assertions.assertInstanceOf(EqualsTo.class, inner.getExpression()); + Assertions.assertInstanceOf(Column.class, equalsTo.getLeftExpression()); + } + + @Test + public void testNotNotCaseInsensitiveIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("NoT nOt 1 = 1"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(EqualsTo.class, inner.getExpression()); + } + + @Test + public void testEightNotIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil + .parseCondExpression("not not not not not not not not 1 = 1"); + int depth = 0; + while (expr instanceof NotExpression) { + expr = ((NotExpression) expr).getExpression(); + depth++; + } + Assertions.assertEquals(8, depth); + Assertions.assertInstanceOf(EqualsTo.class, expr); + } + + @Test + public void testNotNotParenthesizedConditionIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not (a = 1 and b = 2)"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(ParenthesedExpressionList.class, inner.getExpression()); + } + + @Test + public void testNotNotInStatementPositionsIssue1170() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE a IN (SELECT id FROM u WHERE NOT NOT x = 1)"); + TestUtils.assertSqlCanBeParsedAndDeparsed("UPDATE t SET a = 1 WHERE NOT NOT b = 2"); + TestUtils.assertSqlCanBeParsedAndDeparsed("DELETE FROM t WHERE NOT NOT b = 2"); + } + + @Test + public void testValuePositionNotStaysOnOperand() throws JSQLParserException { + Expression equalsTo = CCJSqlParserUtil.parseCondExpression("a = not 1"); + EqualsTo eq = Assertions.assertInstanceOf(EqualsTo.class, equalsTo); + Assertions.assertInstanceOf(Column.class, eq.getLeftExpression()); + Assertions.assertInstanceOf(NotExpression.class, eq.getRightExpression()); + + Expression addition = CCJSqlParserUtil.parseCondExpression("1 + not 2"); + Addition add = Assertions.assertInstanceOf(Addition.class, addition); + Assertions.assertInstanceOf(NotExpression.class, add.getRightExpression()); + } + + @Test + public void testCaretBindsTighterThanMultiplication() throws JSQLParserException { + // PostgreSQL's power and MySQL's XOR both evaluate 2 * 3 ^ 2 as 2 * (3 ^ 2) + Multiplication multiplication = Assertions.assertInstanceOf(Multiplication.class, + CCJSqlParserUtil.parseExpression("2 * 3 ^ 2")); + Assertions.assertInstanceOf(LongValue.class, multiplication.getLeftExpression()); + Assertions.assertInstanceOf(BitwiseXor.class, multiplication.getRightExpression()); + + Division division = Assertions.assertInstanceOf(Division.class, + CCJSqlParserUtil.parseExpression("10 / 2 ^ 2")); + Assertions.assertInstanceOf(BitwiseXor.class, division.getRightExpression()); + + Modulo modulo = Assertions.assertInstanceOf(Modulo.class, + CCJSqlParserUtil.parseExpression("18 % 4 ^ 2")); + Assertions.assertInstanceOf(BitwiseXor.class, modulo.getRightExpression()); + + multiplication = Assertions.assertInstanceOf(Multiplication.class, + CCJSqlParserUtil.parseExpression("2 ^ 3 * 2")); + Assertions.assertInstanceOf(BitwiseXor.class, multiplication.getLeftExpression()); + + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT pi() * r ^ 2 FROM circles"); + } + + @Test + public void testCaretIsLeftAssociative() throws JSQLParserException { + BitwiseXor outer = Assertions.assertInstanceOf(BitwiseXor.class, + CCJSqlParserUtil.parseExpression("2 ^ 3 ^ 2")); + Assertions.assertInstanceOf(BitwiseXor.class, outer.getLeftExpression()); + Assertions.assertInstanceOf(LongValue.class, outer.getRightExpression()); + } + + @Test + public void testBitwiseOperatorsBindLooserThanAddition() throws JSQLParserException { + // PostgreSQL, MySQL and SQLite all evaluate 1 & 7 - 2 as 1 & (7 - 2) + BitwiseAnd and = Assertions.assertInstanceOf(BitwiseAnd.class, + CCJSqlParserUtil.parseExpression("1 & 7 - 2")); + Assertions.assertInstanceOf(LongValue.class, and.getLeftExpression()); + Assertions.assertInstanceOf(Subtraction.class, and.getRightExpression()); + + BitwiseOr or = Assertions.assertInstanceOf(BitwiseOr.class, + CCJSqlParserUtil.parseExpression("a | b + c")); + Assertions.assertInstanceOf(Addition.class, or.getRightExpression()); + + BitwiseLeftShift leftShift = Assertions.assertInstanceOf(BitwiseLeftShift.class, + CCJSqlParserUtil.parseExpression("1 << n - 1")); + Assertions.assertInstanceOf(Subtraction.class, leftShift.getRightExpression()); + + BitwiseRightShift rightShift = Assertions.assertInstanceOf(BitwiseRightShift.class, + CCJSqlParserUtil.parseExpression("a + b >> 2")); + Assertions.assertInstanceOf(Addition.class, rightShift.getLeftExpression()); + + Intersects xor = Assertions.assertInstanceOf(Intersects.class, + CCJSqlParserUtil.parseExpression("a # b + c", false, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + Assertions.assertInstanceOf(Addition.class, xor.getRightExpression()); + + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT flags & 1 << bit - 1 FROM t"); + } + + @Test + public void testBitwiseOperatorsShareOneLeftAssociativeLevel() throws JSQLParserException { + // as in PostgreSQL and SQLite: 4 | 1 & 0 is (4 | 1) & 0 + BitwiseAnd and = Assertions.assertInstanceOf(BitwiseAnd.class, + CCJSqlParserUtil.parseExpression("4 | 1 & 0")); + Assertions.assertInstanceOf(BitwiseOr.class, and.getLeftExpression()); + } + + @Test + public void testSpaceSeparatedConcatStillBindsLikeAddition() throws JSQLParserException { + BitwiseAnd and = Assertions.assertInstanceOf(BitwiseAnd.class, + CCJSqlParserUtil.parseExpression("a & b | | c")); + Assertions.assertInstanceOf(Concat.class, and.getRightExpression()); + } + + @Test + public void testMySqlBitwiseOperatorsAreTiered() throws JSQLParserException { + // MySQL tiers the bitwise operators: << and >> above &, & above | + BitwiseOr or = Assertions.assertInstanceOf(BitwiseOr.class, + CCJSqlParserUtil.parseExpression("1 | 2 & 4", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(BitwiseAnd.class, or.getRightExpression()); + + BitwiseAnd and = Assertions.assertInstanceOf(BitwiseAnd.class, + CCJSqlParserUtil.parseExpression("3 & 1 << 2", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(BitwiseLeftShift.class, and.getRightExpression()); + + // the whole chain still sits below + and -, as in the default dialect + BitwiseAnd shifted = Assertions.assertInstanceOf(BitwiseAnd.class, + CCJSqlParserUtil.parseExpression("1 & 7 - 2", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(Subtraction.class, shifted.getRightExpression()); + } + + @Test + public void testXorBindsBetweenAndAndOr() throws JSQLParserException { + // MySQL: XOR sits between AND and OR + OrExpression or = Assertions.assertInstanceOf(OrExpression.class, + CCJSqlParserUtil.parseExpression("1 OR 1 XOR 1", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(XorExpression.class, or.getRightExpression()); + + XorExpression xor = Assertions.assertInstanceOf(XorExpression.class, + CCJSqlParserUtil.parseExpression("1 XOR 1 AND 1", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(AndExpression.class, xor.getRightExpression()); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/FunctionFeatureAnalysisTest.java b/src/test/java/net/sf/jsqlparser/expression/FunctionFeatureAnalysisTest.java new file mode 100644 index 0000000000..f14bf4b17d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/FunctionFeatureAnalysisTest.java @@ -0,0 +1,142 @@ +/*- + * #%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 static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementFeatures; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.select.Limit; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class FunctionFeatureAnalysisTest { + static Stream functionForms() { + return Stream.of( + Arguments.of("SELECT pg_sleep(1) FROM t", "pg_sleep"), + Arguments.of("SELECT pg_sleep(1) OVER () FROM t", "pg_sleep"), + Arguments.of("SELECT SuM(v) OVER () FROM t", "sum"), + Arguments.of("SELECT analytics.sum(v) OVER () FROM t", "analytics.sum"), + Arguments.of("SELECT CONVERT(v USING utf8) FROM t", "convert"), + Arguments.of("SELECT CONVERT(INT, v) FROM t", "convert"), + Arguments.of("SELECT TRY_CONVERT(INT, v) FROM t", "try_convert")); + } + + @ParameterizedTest + @MethodSource("functionForms") + void appliesTheSamePurityContractToEachFunctionForm(String sql, String name) + throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + String before = statement.toString(); + StatementFeatures unknown = statement.getFeatures(n -> false); + assertThat(unknown.getUnresolvedReferences()).containsExactly(name); + assertThat(unknown.getUncertain()) + .contains(StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA); + assertThat(unknown.modifiesData()).isFalse(); + assertThat(unknown.mayModifyData()).isTrue(); + + StatementFeatures pure = statement.getFeatures(name::equals); + assertThat(pure.getUnresolvedReferences()).isEmpty(); + assertThat(pure.mayModifyData()).isFalse(); + assertThat(statement.toString()).isEqualTo(before); + assertThat(CCJSqlParserUtil.parse(before).toString()).isEqualTo(before); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT sum(danger(v)) OVER () FROM t", + "SELECT sum(v) OVER (PARTITION BY danger(k)) FROM t", + "SELECT sum(v) OVER (ORDER BY danger(k)) FROM t", + "SELECT sum(v) FILTER (WHERE danger(k) > 0) OVER () FROM t", + "SELECT array_agg(v ORDER BY danger(k)) OVER () FROM t", + "SELECT lag(v, danger(k), 0) OVER () FROM t", + "SELECT lag(v, 1, danger(k)) OVER () FROM t", + "SELECT sum(v) OVER (ORDER BY k ROWS danger(1) PRECEDING) FROM t", + "SELECT sum(v) OVER (ORDER BY k ROWS BETWEEN danger(1) PRECEDING AND CURRENT ROW) FROM t", + "SELECT sum(v) OVER (ORDER BY k ROWS BETWEEN CURRENT ROW AND danger(1) FOLLOWING) FROM t", + "SELECT sum(v) OVER w FROM t WINDOW w AS (PARTITION BY danger(k))", + "SELECT sum(v) OVER w FROM t WINDOW w AS (ORDER BY danger(k))", + "SELECT sum(v) OVER w FROM t WINDOW w AS (ORDER BY k ROWS danger(1) PRECEDING)", + "SELECT CONVERT(INT, danger(v)) FROM t"}) + void pureOuterFunctionsDoNotHideUnprovenChildren(String sql) throws JSQLParserException { + StatementFeatures features = CCJSqlParserUtil.parse(sql) + .getFeatures(Set.of("sum", "array_agg", "lag", "convert")::contains); + assertThat(features.getUnresolvedReferences()).containsExactly("danger"); + assertThat(features.mayModifyData()).isTrue(); + assertThat(features.modifiesData()).isFalse(); + } + + @Test + void inlineWindowVisitsBothOrderListsOnceAndKeepsContext() throws JSQLParserException { + String sql = "SELECT array_agg(arg_fn(v) ORDER BY inner_fn(k)) " + + "FILTER (WHERE filter_fn(v) > 0) OVER (PARTITION BY part_fn(k) " + + "ORDER BY outer_fn(k) ROWS BETWEEN start_fn(1) PRECEDING " + + "AND end_fn(1) FOLLOWING) FROM t"; + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql); + List seen = new ArrayList<>(); + Object marker = new Object(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Function function, S context) { + assertThat(context).isSameAs(marker); + seen.add(function.getName()); + return super.visit(function, context); + } + + @Override + protected Void visitExpressions(Expression expression, S context, + Collection children) { + assertThat(context).isSameAs(marker); + return super.visitExpressions(expression, context, children); + } + }; + select.accept(new SelectVisitorAdapter<>(expressions), marker); + assertThat(seen).containsExactly("arg_fn", "filter_fn", "inner_fn", "part_fn", + "outer_fn", "start_fn", "end_fn"); + } + + @Test + void functionModifiersRemainVisibleAfterAnalyticConversion() { + Function function = new Function().withName("parent").withParameters(new LongValue(1)); + function.setHavingClause(new Function.HavingClause(Function.HavingClause.HavingType.MAX, + new Function().withName("having_fn"))); + function.setKeywordArguments(List.of(new Function.KeywordArgument("SEPARATOR", + new Function().withName("keyword_fn")))); + function.setLimit(new Limit().withRowCount(new Function().withName("limit_fn"))); + for (Expression expression : List.of(function, new AnalyticExpression(function))) { + PlainSelect select = new PlainSelect(); + select.setSelectItems(List.of(new SelectItem<>(expression))); + assertThat(select.getFeatures("parent"::equals).getUnresolvedReferences()) + .containsExactly("having_fn", "keyword_fn", "limit_fn"); + } + } + + @Test + void emptyAndUnboundedWindowsContainNoSpuriousFunctions() throws JSQLParserException { + assertThat(new WindowDefinition().getAllExpressions()).isEmpty(); + Statement statement = CCJSqlParserUtil.parse("SELECT sum(v) OVER " + + "(ORDER BY k ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t"); + assertThat(statement.getFeatures("sum"::equals).mayModifyData()).isFalse(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/FunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/FunctionTest.java index deb508c199..6456918b15 100644 --- a/src/test/java/net/sf/jsqlparser/expression/FunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/FunctionTest.java @@ -157,4 +157,14 @@ void testAesDecryptWithKeyExpressionInSelect() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testBracketedOperandAndEmptyListArguments() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT f((1 + a) / (1 + b), (c + d) * 2) FROM t", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT f(()) FROM t", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed("INSERT INTO t VALUES ()", true); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java index 5fc72bea8e..b31efae37c 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.select.PlainSelect; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -39,6 +40,21 @@ void testIssue1792() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + @Test + void testHashArrowOperatorsRoundTrip() throws JSQLParserException { + // #> and #>> must survive deparse+reparse: without spaces the operator glues + // onto the object (a#>'{b}') and re-parses as the comparison a# > '{b}'. + for (String sqlStr : new String[] {"SELECT a #> '{b}' FROM t", + "SELECT a #>> '{b}' FROM t"}) { + PlainSelect st = (PlainSelect) CCJSqlParserUtil.parse(sqlStr); + Assertions.assertInstanceOf(JsonExpression.class, st.getSelectItem(0).getExpression()); + PlainSelect reparsed = (PlainSelect) CCJSqlParserUtil.parse(st.toString()); + Assertions.assertInstanceOf(JsonExpression.class, + reparsed.getSelectItem(0).getExpression(), + sqlStr + " deparsed to " + st); + } + } + @Test void testSnowflakeGetOperator() throws JSQLParserException { // https://docs.snowflake.com/en/user-guide/querying-semistructured diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonTableCollationTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonTableCollationTest.java new file mode 100644 index 0000000000..84eac19a7e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/JsonTableCollationTest.java @@ -0,0 +1,86 @@ +/*- + * #%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 net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.JsonTableFunction.JsonTableValueColumnDefinition; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.TableFunction; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class JsonTableCollationTest { + @ParameterizedTest + @ValueSource(strings = { + "n VARCHAR(10) COLLATE utf8mb4_bin PATH '$.n'", + "n VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin PATH '$.n'", + "n VARCHAR(10) COLLATE `utf8mb4_bin` PATH '$.n'", + "n VARCHAR(10) COLLATE 'utf8mb4_bin' PATH '$.n'", + "n VARCHAR(10) COLLATE \"utf8mb4_bin\" PATH '$.n'", + "NESTED PATH '$.items[*]' COLUMNS (n VARCHAR(10) COLLATE utf8mb4_bin PATH '$.n')" + }) + void collatedColumnsRoundTrip(String column) throws JSQLParserException { + PlainSelect select = parse(sql(column)); + parse(select.toString()); + } + + @Test + void collationCanBeInspectedEditedRemovedAndConstructed() throws JSQLParserException { + PlainSelect select = parse(sql("n VARCHAR(10) COLLATE utf8mb4_bin PATH '$.n'")); + JsonTableFunction table = + (JsonTableFunction) ((TableFunction) select.getFromItem()).getFunction(); + JsonTableValueColumnDefinition column = + (JsonTableValueColumnDefinition) table.getColumnsClause().getColumnDefinitions() + .get(0); + assertEquals("utf8mb4_bin", column.getCollation()); + assertEquals("VARCHAR", column.getDataType().getBaseTypeName()); + column.setCollation("utf8mb4_general_ci"); + TestUtils.assertStatementCanBeDeparsedAs(select, + sql("n VARCHAR(10) COLLATE utf8mb4_general_ci PATH '$.n'"), true); + parse(select.toString()); + column.setCollation(null); + TestUtils.assertStatementCanBeDeparsedAs(select, sql("n VARCHAR(10) PATH '$.n'"), true); + parse(select.toString()); + JsonTableValueColumnDefinition constructed = new JsonTableValueColumnDefinition() + .setColumnName("v") + .setDataType(new ColDataType("VARCHAR").addArgumentsStringList("10")) + .setCollation("utf8mb4_bin").setPathExpression(new StringValue("$.v")); + table.getColumnsClause().getColumnDefinitions().set(0, constructed); + TestUtils.assertStatementCanBeDeparsedAs(select, + sql("v VARCHAR(10) COLLATE utf8mb4_bin PATH '$.v'"), true); + parse(select.toString()); + } + + @Test + void columnCollationRequiresMySqlDialect() { + String sql = sql("n VARCHAR(10) COLLATE utf8mb4_bin PATH '$.n'"); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + } + + private static PlainSelect parse(String sql) throws JSQLParserException { + return (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static String sql(String column) { + return "SELECT * FROM JSON_TABLE('[{\"n\":\"ab\"}]', '$[*]' COLUMNS (" + column + + ")) AS jt"; + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonTableExistsTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonTableExistsTest.java new file mode 100644 index 0000000000..6f7a4a38a6 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/JsonTableExistsTest.java @@ -0,0 +1,85 @@ +/*- + * #%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 net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.JsonTableFunction.JsonTableValueColumnDefinition; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.TableFunction; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class JsonTableExistsTest { + @ParameterizedTest + @ValueSource(strings = { + "n INT EXISTS PATH '$.n'", + "n VARCHAR(10) EXISTS PATH '$.n'", + "NESTED PATH '$.items[*]' COLUMNS (n INT EXISTS PATH '$.n')" + }) + void typedExistsRoundTrips(String column) throws JSQLParserException { + String sql = sql(column); + for (Dialect dialect : new Dialect[] {null, Dialect.MYSQL, Dialect.ORACLE}) { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, + true, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false, + parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + } + } + + @Test + void typedExistsCanBeInspectedAndEdited() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql("n INT EXISTS PATH '$.n'")); + JsonTableFunction table = + (JsonTableFunction) ((TableFunction) select.getFromItem()).getFunction(); + JsonTableValueColumnDefinition column = + (JsonTableValueColumnDefinition) table.getColumnsClause().getColumnDefinitions() + .get(0); + assertTrue(column.isExists()); + assertEquals("INT", column.getDataType().getBaseTypeName()); + column.setColumnName("present").setDataType(new ColDataType("BIGINT")) + .setPathExpression(new StringValue("$.other")); + TestUtils.assertStatementCanBeDeparsedAs(select, + sql("present BIGINT EXISTS PATH '$.other'"), true); + TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString()); + column.setExistsKeyword(false); + TestUtils.assertStatementCanBeDeparsedAs(select, sql("present BIGINT PATH '$.other'"), + true); + } + + @Test + void constructedAndUntypedExistsColumnsRetainOrder() throws JSQLParserException { + JsonTableValueColumnDefinition column = new JsonTableValueColumnDefinition() + .setColumnName("n").setExistsKeyword(true).setDataType(new ColDataType("INT")) + .setPathExpression(new StringValue("$.n")); + assertEquals("n INT EXISTS PATH '$.n'", column.toString()); + column.setDataType(null); + assertEquals("n EXISTS PATH '$.n'", column.toString()); + TestUtils.assertSqlCanBeParsedAndDeparsed(sql(column.toString())); + } + + private static String sql(String column) { + return "SELECT * FROM JSON_TABLE('[{\"n\":1},{}]', '$[*]' COLUMNS (" + column + ")) AS jt"; + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonTableResponseTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonTableResponseTest.java new file mode 100644 index 0000000000..a63d5aa808 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/JsonTableResponseTest.java @@ -0,0 +1,106 @@ +/*- + * #%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 net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.JsonTableFunction.JsonTableValueColumnDefinition; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.TableFunction; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class JsonTableResponseTest { + @ParameterizedTest + @ValueSource(strings = { + "DEFAULT '9' ON ERROR", + "DEFAULT '0' ON EMPTY DEFAULT '9' ON ERROR", + "DEFAULT '9' ON ERROR DEFAULT '0' ON EMPTY", + "NULL ON EMPTY DEFAULT '9' ON ERROR", + "ERROR ON EMPTY DEFAULT '9' ON ERROR" + }) + void defaultsRoundTripInEitherOrder(String response) throws JSQLParserException { + String sql = sql("n INT PATH '$.n' " + response); + for (Dialect dialect : new Dialect[] {null, Dialect.MYSQL, Dialect.ORACLE}) { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, + true, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false, + parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + } + } + + @Test + void defaultExpressionsAreVisitedAndEditable() throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + sql("n INT PATH '$.n' DEFAULT '0' ON EMPTY DEFAULT '9' ON ERROR")); + JsonTableFunction table = + (JsonTableFunction) ((TableFunction) select.getFromItem()).getFunction(); + JsonTableValueColumnDefinition column = + (JsonTableValueColumnDefinition) table.getColumnsClause().getColumnDefinitions() + .get(0); + assertEquals(JsonFunction.JsonOnResponseBehaviorType.DEFAULT, + column.getOnErrorBehavior().getType()); + assertEquals("9", ((StringValue) column.getOnErrorBehavior().getExpression()).getValue()); + List visited = new ArrayList<>(); + table.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + visited.add(value.getValue()); + if ("9".equals(value.getValue())) { + value.setValue("7"); + } + return null; + } + }, null); + assertTrue(visited.containsAll(List.of("$.n", "0", "9"))); + TestUtils.assertStatementCanBeDeparsedAs(select, + sql("n INT PATH '$.n' DEFAULT '0' ON EMPTY DEFAULT '7' ON ERROR"), true); + column.setOnErrorBehavior(new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.DEFAULT, new StringValue("8"))); + column.setOnEmptyAfterOnError(true); + TestUtils.assertStatementCanBeDeparsedAs(select, + sql("n INT PATH '$.n' DEFAULT '8' ON ERROR DEFAULT '0' ON EMPTY"), true); + TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString()); + } + + @Test + void nestedDefaultsRoundTrip() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed(sql( + "NESTED PATH '$.items[*]' COLUMNS (n INT PATH '$.n' DEFAULT '9' ON ERROR)")); + } + + @ParameterizedTest + @ValueSource( + strings = {"NULL", "ERROR", "TRUE", "FALSE", "EMPTY", "EMPTY ARRAY", "EMPTY OBJECT"}) + void sharedNonDefaultResponsesRoundTrip(String behavior) throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed(sql("n PATH '$.n' " + behavior + " ON ERROR")); + TestUtils.assertSqlCanBeParsedAndDeparsed(sql("n PATH '$.n' " + behavior + " ON EMPTY")); + } + + private static String sql(String column) { + return "SELECT * FROM JSON_TABLE('[{\"n\":1},{},{\"n\":\"bad\"}]', '$[*]' COLUMNS (" + + column + ")) AS jt"; + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/LambdaExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/LambdaExpressionTest.java index 4dcc4c6e16..bd6a0fcd8a 100644 --- a/src/test/java/net/sf/jsqlparser/expression/LambdaExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/LambdaExpressionTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -20,7 +21,10 @@ class LambdaExpressionTest { @Test void testLambdaFunctionSingleParameter() throws JSQLParserException { String sqlStr = "select list_transform( split('test', ''), x -> unicode(x) )"; - TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Select select = assertInstanceOf( + Select.class, TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true)); + Function function = select.getPlainSelect().getSelectItem(0).getExpression(Function.class); + assertInstanceOf(LambdaExpression.class, function.getParameters().get(1)); } @Test @@ -52,4 +56,15 @@ void testLambdaFirstArgumentIssue2195() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + @Test + void testLambdaFunctionFourParameters() throws JSQLParserException { + String sqlStr = "SELECT list_transform([1], (a, b, c, d) -> a)"; + Select select = assertInstanceOf( + Select.class, TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true)); + Function function = select.getPlainSelect().getSelectItem(0).getExpression(Function.class); + LambdaExpression lambda = + assertInstanceOf(LambdaExpression.class, function.getParameters().get(1)); + assertEquals(4, lambda.getIdentifiers().size()); + } + } diff --git a/src/test/java/net/sf/jsqlparser/expression/LambdaRoundTripTest.java b/src/test/java/net/sf/jsqlparser/expression/LambdaRoundTripTest.java new file mode 100644 index 0000000000..7c5f94dc80 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/LambdaRoundTripTest.java @@ -0,0 +1,136 @@ +/*- + * #%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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +class LambdaRoundTripTest { + + @ParameterizedTest + @ValueSource(strings = {"SELECT arrayMap((x) -> x * 2, [1, 2])", + "SELECT list_transform([1, 2], (x) -> x + 1)", + "SELECT f(1, [1, 2], (x) -> CASE WHEN x > 1 THEN x ELSE 0 END)", + "SELECT arrayMap((x, y) -> x + y, [1], [2])", + "SELECT list_transform([1], (x) -> list_transform([2], (y) -> x + y))"}) + void preservesParameterGroupingAndNestedAst(String sql) throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql); + List before = lambdas(select); + assertFalse(before.isEmpty()); + assertTrue(before.stream().allMatch(LambdaExpression::isParenthesized)); + StringBuilder visitorSql = new StringBuilder(); + select.accept(new StatementDeParser(visitorSql)); + for (String rendered : List.of(select.toString(), visitorSql.toString())) { + PlainSelect reparsed = (PlainSelect) CCJSqlParserUtil.parse(rendered); + List after = lambdas(reparsed); + assertEquals(before.size(), after.size(), rendered); + for (int i = 0; i < before.size(); i++) { + assertEquals(before.get(i).getIdentifiers(), after.get(i).getIdentifiers()); + assertEquals(before.get(i).getExpression().getClass(), + after.get(i).getExpression().getClass()); + assertTrue(after.get(i).isParenthesized()); + } + assertEquals(select.toString(), reparsed.toString()); + } + } + + @Test + void keepsUnparenthesizedSingleParameterRendering() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT list_transform([1], x -> x + 1)"); + assertFalse(lambdas(select).get(0).isParenthesized()); + assertEquals("SELECT list_transform([1], x -> x + 1)", select.toString()); + LambdaExpression constructed = new LambdaExpression("x", new LongValue(1)); + assertEquals("x -> 1", constructed.toString()); + constructed.setParenthesized(true); + assertEquals("( x ) -> 1", constructed.toString()); + } + + @Test + void sharedRendererStillVisitsAndRewritesLambdaBody() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT arrayMap((x) -> x + 1, [2])"); + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 10); + } + }; + select.accept(new StatementDeParser(expressions, + new net.sf.jsqlparser.util.deparser.SelectDeParser(), sql)); + assertEquals("SELECT arrayMap(( x ) -> x + 11, [12])", sql.toString()); + assertEquals("SELECT arrayMap(( x ) -> x + 1, [2])", select.toString()); + assertEquals(1, lambdas((PlainSelect) CCJSqlParserUtil.parse(sql.toString())).size()); + } + + // All fixture statements execute on PostgreSQL 18.6 or MySQL 8.4.11, respectively. + @ParameterizedTest + @CsvFileSource(resources = "/net/sf/jsqlparser/expression/arrow-dialect-cases.tsv", + delimiter = '\t') + void jsonArrowRemainsJsonInEveryArgumentPosition(Dialect dialect, String sql) + throws JSQLParserException { + for (boolean complex : List.of(false, true)) { + PlainSelect select = parseJson(sql, dialect, complex); + StringBuilder visitorSql = new StringBuilder(); + select.accept(new StatementDeParser(visitorSql)); + for (String rendered : List.of(sql, select.toString(), visitorSql.toString())) { + PlainSelect reparsed = parseJson(rendered, dialect, complex); + assertTrue(lambdas(reparsed).isEmpty(), rendered); + Function function = assertInstanceOf(Function.class, + reparsed.getSelectItem(0).getExpression()); + assertTrue(function.getParameters().stream() + .anyMatch(JsonExpression.class::isInstance), rendered); + } + } + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "POSTGRESQL"}) + void rejectsMissingJsonOperand(Dialect dialect) { + assertThrows(JSQLParserException.class, + () -> parseJson("SELECT COALESCE(NULL, payload ->)", dialect, true)); + } + + private static PlainSelect parseJson(String sql, Dialect dialect, boolean complex) + throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(dialect).withAllowComplexParsing(complex)); + } + + private static List lambdas(PlainSelect select) { + List result = new ArrayList<>(); + select.getSelectItem(0).getExpression().accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(LambdaExpression expression, S context) { + result.add(expression); + return super.visit(expression, context); + } + }); + return result; + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/LimitExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/LimitExpressionTest.java index 70dbd21038..3139530c6d 100644 --- a/src/test/java/net/sf/jsqlparser/expression/LimitExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/LimitExpressionTest.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; @@ -31,6 +32,38 @@ public void testIssue933() throws JSQLParserException { "SELECT * FROM tmp3 LIMIT (SELECT 2)", true); } + @Test + public void testIssue2359() throws JSQLParserException { + // PostgreSQL allows any expression, including a scalar subquery, as the LIMIT row + // count. A long parenthesized subquery used to fail because the ClickHouse + // "LIMIT ... BY ..." branch was chosen by a numeric LOOKAHEAD that cannot see past + // the subquery. + String sql = "WITH some_table AS (SELECT 1 AS some_column), " + + "another_table AS (SELECT 'some_value' AS condition_column) " + + "SELECT some_column FROM some_table ORDER BY some_column " + + "LIMIT (SELECT COUNT(*) FROM another_table WHERE condition_column = 'some_value')"; + + PlainSelect plainSelect = (PlainSelect) CCJSqlParserUtil.parse(sql); + Assertions.assertTrue( + plainSelect.getLimit().getRowCount() instanceof ParenthesedSelect); + Assertions.assertNull(plainSelect.getLimitBy()); + + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + + // A function wrapping a scalar subquery must work as the row count too. + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a FROM t LIMIT GREATEST(0, (SELECT COUNT(*) FROM u WHERE c = 'x'))", + true); + } + + @Test + public void testLimitByClickHouseUnchanged() throws JSQLParserException { + // ClickHouse "LIMIT ... BY ..." must keep parsing and round-tripping after the LIMIT + // row-count disambiguation was rewritten (issue #2359). + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT id FROM t LIMIT 5 BY id", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT id FROM t LIMIT 2, 5 BY id", true); + } + @Test public void testIssue1373() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed( diff --git a/src/test/java/net/sf/jsqlparser/expression/MySqlConvertTest.java b/src/test/java/net/sf/jsqlparser/expression/MySqlConvertTest.java new file mode 100644 index 0000000000..94c0c04dd2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/MySqlConvertTest.java @@ -0,0 +1,99 @@ +/*- + * #%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 static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlConvertTest { + @ParameterizedTest + @ValueSource(strings = {"'42', SIGNED", "'-1', UNSIGNED INTEGER", + "'3.14', DECIMAL(10,2)", "'xx', CHAR(16) CHARACTER SET 'utf8mb4'", + "'xx', CHAR(16) CHARSET binary", "'2026-09-21', DATE", + "NULL, CHAR", "amount, DECIMAL(12,4)", "COALESCE(amount, 0), SIGNED", + "CONVERT('42', SIGNED), CHAR(10)", "?, SIGNED"}) + void representsExpressionBeforeType(String arguments) throws Exception { + PlainSelect select = parse("SELECT CONVERT(" + arguments + ") FROM t", Dialect.MYSQL); + TranscodingFunction convert = conversion(select); + assertEquals(TranscodingFunction.Syntax.TYPE_LAST, convert.getSyntax()); + assertFalse(convert.isTranscodeStyle()); + assertNotNull(convert.getColDataType()); + roundTrip(select, Dialect.MYSQL); + } + + @Test + void distinguishesDialectDependentOrderAndLegacyAccessors() throws Exception { + TranscodingFunction mysql = conversion(parse("SELECT CONVERT(value, CHAR)", Dialect.MYSQL)); + assertInstanceOf(Column.class, mysql.getExpression()); + assertEquals("CHAR", mysql.getColDataType().getDataType()); + TranscodingFunction sqlserver = conversion( + parse("SELECT CONVERT(VARCHAR(10), value, 120)", Dialect.SQLSERVER)); + assertEquals(TranscodingFunction.Syntax.TYPE_FIRST, sqlserver.getSyntax()); + assertEquals("120", sqlserver.getTranscodingName()); + roundTrip(parse("SELECT TRY_CONVERT(INT, value)", Dialect.SQLSERVER), Dialect.SQLSERVER); + roundTrip(parse("SELECT CONVERT(VARCHAR(10), value, 120)", Dialect.SQLSERVER), + Dialect.SQLSERVER); + TranscodingFunction using = + conversion(parse("SELECT CONVERT(value USING utf8mb4)", Dialect.MYSQL)); + assertTrue(using.isTranscodeStyle()); + using.setTranscodeStyle(false); + assertEquals(TranscodingFunction.Syntax.TYPE_FIRST, using.getSyntax()); + using.setTranscodeStyle(true); + assertEquals(TranscodingFunction.Syntax.USING, using.getSyntax()); + } + + @Test + void visitsOperandAndRendersTypeMutations() throws Exception { + PlainSelect select = parse("SELECT CONVERT(42, SIGNED)", Dialect.MYSQL); + conversion(select).getColDataType().setDataType("UNSIGNED"); + StringBuilder out = new StringBuilder(); + ExpressionDeParser visitor = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 1); + } + }; + select.accept(new StatementDeParser(visitor, new SelectDeParser(), out), null); + assertEquals("SELECT CONVERT( 43, UNSIGNED )", out.toString()); + roundTrip(select, Dialect.MYSQL); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT CONVERT(1,)", "SELECT CONVERT(, SIGNED)", + "SELECT CONVERT(1, SIGNED, 120)", "SELECT CONVERT(1 USING)"}) + void rejectsIncompleteArguments(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL)); + } + + private static PlainSelect parse(String sql, Dialect dialect) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + } + + private static TranscodingFunction conversion(PlainSelect select) { + return assertInstanceOf(TranscodingFunction.class, select.getSelectItem(0).getExpression()); + } + + private static void roundTrip(PlainSelect select, Dialect dialect) throws Exception { + StringBuilder out = new StringBuilder(); + select.accept(new StatementDeParser(out), null); + assertEquals(select.toString(), out.toString()); + assertEquals(select.toString(), parse(out.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/OracleHintTest.java b/src/test/java/net/sf/jsqlparser/expression/OracleHintTest.java index 2da69721c2..687c29ad57 100644 --- a/src/test/java/net/sf/jsqlparser/expression/OracleHintTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/OracleHintTest.java @@ -9,6 +9,10 @@ */ package net.sf.jsqlparser.expression; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.time.Duration; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -46,4 +50,18 @@ void testMerge() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true); } + @Test + void testCraftedHintCommentDoesNotBacktrack() { + final StringBuilder sb = new StringBuilder("-- /*+ "); + for (int i = 0; i < 100000; i++) { + sb.append('*'); + } + final String crafted = sb.toString(); + + // a line comment carrying an unterminated /*+ marker (no closing */) used to make the + // block hint pattern backtrack quadratically, and it is not an oracle hint anyway + assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> assertFalse(OracleHint.isHintMatch(crafted))); + } + } diff --git a/src/test/java/net/sf/jsqlparser/expression/OrderedSetFilterTest.java b/src/test/java/net/sf/jsqlparser/expression/OrderedSetFilterTest.java new file mode 100644 index 0000000000..72f783fea3 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/OrderedSetFilterTest.java @@ -0,0 +1,89 @@ +/*- + * #%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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class OrderedSetFilterTest { + @ParameterizedTest + @ValueSource(strings = { + "percentile_cont(0.5) WITHIN GROUP (ORDER BY score) FILTER (WHERE active)", + "percentile_disc(0.5) WITHIN GROUP (ORDER BY score DESC NULLS LAST) " + + "FILTER (WHERE active AND score > 0)", + "mode() WITHIN GROUP (ORDER BY score) FILTER (WHERE active)", + "rank(5) WITHIN GROUP (ORDER BY score) FILTER (WHERE NOT active)", + "sum(score) FILTER (WHERE active)", + "sum(score) FILTER (WHERE active) OVER (PARTITION BY team)", + "percentile_cont(0.5) WITHIN GROUP (ORDER BY score)" + }) + void preservesAggregateClauses(String expression) throws JSQLParserException { + String sql = "SELECT " + expression + " FROM measurements"; + PlainSelect select = parse(sql); + StringBuilder output = new StringBuilder(); + select.accept(new StatementDeParser(output), null); + assertEquals(sql, select.toString().replace(" )", ")")); + assertEquals(sql, output.toString().replace(" )", ")")); + assertEquals(select.toString(), parse(output.toString()).toString()); + assertEquals(select.toString(), CCJSqlParserUtil.parse(sql).toString()); + } + + @Test + void visitsBothOrderingAndFilterExpressions() throws JSQLParserException { + PlainSelect select = parse("SELECT rank(5) WITHIN GROUP (ORDER BY score + 1) " + + "FILTER (WHERE score > 2) FROM measurements"); + AnalyticExpression aggregate = assertInstanceOf(AnalyticExpression.class, + select.getSelectItem(0).getExpression()); + assertEquals(AnalyticType.WITHIN_GROUP, aggregate.getType()); + assertEquals("score > 2", aggregate.getFilterExpression().toString()); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 10); + } + }; + select.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + String expected = "SELECT rank(15) WITHIN GROUP (ORDER BY score + 11) " + + "FILTER (WHERE score > 12) FROM measurements"; + assertEquals(expected, output.toString()); + assertEquals(expected, parse(output.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "percentile_cont(0.5) FILTER (WHERE active) WITHIN GROUP (ORDER BY score)", + "percentile_cont(0.5) WITHIN GROUP (ORDER BY score) FILTER (active)", + "percentile_cont(0.5) WITHIN GROUP (ORDER BY score) FILTER (WHERE)", + "percentile_cont(0.5) WITHIN GROUP (ORDER BY score) " + + "FILTER (WHERE active) FILTER (WHERE active)", + "sum(score) OVER (PARTITION BY team) FILTER (WHERE active)" + }) + void rejectsInvalidPostgresFilterPlacement(String expression) { + assertThrows(JSQLParserException.class, + () -> parse("SELECT " + expression + " FROM measurements")); + } + + private static PlainSelect parse(String sql) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/QuotedHexValueTest.java b/src/test/java/net/sf/jsqlparser/expression/QuotedHexValueTest.java new file mode 100644 index 0000000000..2902220374 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/QuotedHexValueTest.java @@ -0,0 +1,81 @@ +/*- + * #%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 net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class QuotedHexValueTest { + @ParameterizedTest + @ValueSource(strings = { + "SELECT X'6162' AS v", + "SELECT X'6162' AS v", + "SELECT X'6162' FROM t", + "SELECT X'6162' ", + "SELECT X'6162'\tAS v", + "SELECT X'6162'\nAS v" + }) + void whitespaceIsNotPartOfHexLiteral(String sql) throws JSQLParserException { + for (Dialect dialect : new Dialect[] {null, Dialect.MYSQL, Dialect.POSTGRESQL}) { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql, + parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + HexValue hex = (HexValue) select.getSelectItem(0).getExpression(); + assertEquals("X'6162'", hex.getValue()); + assertEquals("6162", hex.getDigits()); + assertEquals(24930L, hex.getLong()); + assertEquals("ab", hex.getStringValue().getValue()); + assertEquals("\\x61\\x62", hex.getBlob().getValue()); + String canonical = select.toString(); + for (int i = 0; i < 3; i++) { + select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(canonical, + false, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + assertEquals(canonical, select.toString()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"X'61 62'", "X'61' '62'", "X'61''62'", "X''", "x'6162'", "0x6162"}) + void existingHexTokenFormsArePreserved(String literal) throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT " + literal + " AS v"); + assertEquals(literal, ((HexValue) select.getSelectItem(0).getExpression()).getValue()); + TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false); + } + + @Test + void jsonDefaultHexIsAccessibleAndEditable() throws JSQLParserException { + String sql = "SELECT JSON_VALUE('{}', '$.v' DEFAULT X'6162' ON EMPTY)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false); + JsonFunction function = (JsonFunction) select.getSelectItem(0).getExpression(); + HexValue hex = (HexValue) function.getOnEmptyBehavior().getExpression(); + assertEquals("6162", hex.getDigits()); + assertEquals("ab", hex.getStringValue().getValue()); + hex.setValue("X'6364'"); + TestUtils.assertStatementCanBeDeparsedAs(select, sql.replace("6162", "6364")); + TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false); + assertEquals("cd", hex.getStringValue().getValue()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/SQLServerHintsTest.java b/src/test/java/net/sf/jsqlparser/expression/SQLServerHintsTest.java new file mode 100644 index 0000000000..c554d0bc24 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/SQLServerHintsTest.java @@ -0,0 +1,102 @@ +/*- + * #%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 static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.SQLServerHints.LockHint; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SQLServerHintsTest { + @ParameterizedTest + @EnumSource(LockHint.class) + void parsesEachLockingHint(LockHint hint) throws Exception { + Statement statement = parse("SELECT * FROM dbo.jobs WITH (" + hint + ")"); + assertEquals(List.of(hint), hints(statement).getLockHints()); + roundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * FROM dbo.jobs WITH (UPDLOCK, ROWLOCK)", + "SELECT * FROM [dbo].[jobs] AS j WITH (INDEX ([ix_jobs]), UPDLOCK, ROWLOCK) WHERE j.id = 1", + "SELECT * FROM jobs WITH (ROWLOCK, READPAST, UPDLOCK) ORDER BY id", + "SELECT * FROM jobs j WITH (HOLDLOCK) JOIN workers w WITH (ROWLOCK, NOWAIT) ON j.id = w.id", + "WITH q AS (SELECT * FROM jobs WITH (UPDLOCK, ROWLOCK)) SELECT * FROM q", + "UPDATE j SET state = 1 FROM jobs j WITH (UPDLOCK, ROWLOCK) WHERE id = 1", + "DELETE j FROM jobs j WITH (ROWLOCK) WHERE id = 1", + "DELETE FROM jobs WITH (ROWLOCK) WHERE id = 1", + "UPDATE jobs WITH (ROWLOCK, UPDLOCK) SET state = 1 WHERE id = 1", + "SELECT * FROM jobs WITH (NOLOCK)", + "SELECT * FROM jobs WITH (INDEX (ix_jobs), NOLOCK)"}) + void retainsHintsAcrossAliasesJoinsCtesAndDml(String sql) throws Exception { + roundTrip(parse(sql)); + } + + @Test + void exposesTypedHintsWithoutChangingLegacyAccessors() throws Exception { + Statement statement = parse("SELECT * FROM jobs WITH (updlock, rowlock)"); + SQLServerHints hints = hints(statement); + assertEquals(List.of(LockHint.UPDLOCK, LockHint.ROWLOCK), hints.getLockHints()); + hints.getLockHints().set(0, LockHint.READPAST); + hints.setIndexName("ix_jobs"); + assertEquals("SELECT * FROM jobs WITH (INDEX (ix_jobs), READPAST, ROWLOCK)", + statement.toString()); + assertThat(new TablesNamesFinder().getTables(statement)).containsExactly("jobs"); + roundTrip(statement); + + SQLServerHints legacy = new SQLServerHints(); + assertNull(legacy.getNoLock()); + legacy.withNoLock().withIndexName("ix"); + assertEquals(Boolean.TRUE, legacy.getNoLock()); + assertEquals(" WITH (INDEX (ix), NOLOCK)", legacy.toString()); + legacy.setNoLock(false); + assertEquals(Boolean.FALSE, legacy.getNoLock()); + assertEquals(" WITH (INDEX (ix))", legacy.toString()); + legacy.withNoLock(null); + assertNull(legacy.getNoLock()); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * FROM jobs WITH ()", "SELECT * FROM jobs WITH (ROWLOCK,)", + "SELECT * FROM jobs WITH (,UPDLOCK)", "SELECT * FROM jobs WITH (UPDLOCK(1))", + "SELECT * FROM jobs WITH (ROWLOCK = 1)", "SELECT * FROM jobs WITH (UNKNOWN_HINT)", + "SELECT * FROM jobs WITH ('ROWLOCK')", "SELECT * FROM jobs WITH (ROWLOCK"}) + void rejectsMalformedOrUnknownHints(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static SQLServerHints hints(Statement statement) { + return ((Table) ((PlainSelect) statement).getFromItem()).getSqlServerHints(); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.SQLSERVER)); + } + + private static void roundTrip(Statement statement) throws Exception { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/StringValueTest.java b/src/test/java/net/sf/jsqlparser/expression/StringValueTest.java index 3dcb03a3dd..5089720ba5 100644 --- a/src/test/java/net/sf/jsqlparser/expression/StringValueTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/StringValueTest.java @@ -10,10 +10,21 @@ package net.sf.jsqlparser.expression; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import java.time.Duration; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.table.ColumnOption; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -50,6 +61,274 @@ public void testGetNotExcapedValue() { assertEquals(expResult, result); } + @Test + public void testGetNotExcapedValueLargeInputIsLinear() { + // a string literal made of many escaped single quotes used to be unescaped with a + // deleteCharAt() per pair, which is quadratic and lets a modest literal burn CPU + int pairs = 1_000_000; + StringBuilder sb = new StringBuilder(2 * pairs + 2); + sb.append('\''); + for (int i = 0; i < pairs; i++) { + sb.append("''"); + } + sb.append('\''); + StringValue instance = new StringValue(sb.toString()); + + String result = assertTimeoutPreemptively(Duration.ofSeconds(2), + instance::getNotExcapedValue); + + assertEquals(pairs, result.length()); + for (int i = 0; i < result.length(); i++) { + assertEquals('\'', result.charAt(i)); + } + } + + private static StringValue literal(String sql, Dialect dialect) throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT " + sql, + parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + return (StringValue) select.getSelectItem(0).getExpression(); + } + + @Test + void testMySqlBackslashEscapes() throws JSQLParserException { + for (Dialect dialect : new Dialect[] {Dialect.MYSQL, Dialect.MARIADB}) { + StringValue value = literal("'it\\'s a\\\\b'", dialect); + assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, value.getEscaping()); + assertEquals("it\\'s a\\\\b", value.getValue()); + assertEquals("it's a\\b", value.getNotExcapedValue()); + assertEquals("it's", literal("'it''s'", dialect).getNotExcapedValue()); + } + assertEquals("x\ny\tz\0\u001A\b\r", + literal("'x\\ny\\tz\\0\\Z\\b\\r'", Dialect.MYSQL).getNotExcapedValue()); + // \% and \_ keep their backslash; other escaped characters stand for themselves + assertEquals("a\\%b\\_cq\"", literal("'a\\%b\\_c\\q\\\"'", Dialect.MYSQL) + .getNotExcapedValue()); + assertEquals("it's", literal("N'it\\'s'", Dialect.MYSQL).getNotExcapedValue()); + } + + @Test + void testDoubledQuoteFollowsTheQuoteCharacter() throws JSQLParserException { + assertEquals("it\"s", literal("\"it\"\"s\"", Dialect.MYSQL).getNotExcapedValue()); + assertEquals("it''s", literal("\"it''s\"", Dialect.MYSQL).getNotExcapedValue()); + } + + @Test + void testBackslashEscapesWithoutDialect() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT 'it\\'s'", + parser -> parser.withBackslashEscapeCharacter(true)); + StringValue value = (StringValue) select.getSelectItem(0).getExpression(); + assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, value.getEscaping()); + assertEquals("it's", value.getNotExcapedValue()); + + StringValue standard = literal("'a\\nb'", null); + assertEquals(StringValue.Escaping.STANDARD, standard.getEscaping()); + assertEquals("a\\nb", standard.getNotExcapedValue()); + } + + @Test + void testPostgresEscapeStrings() throws JSQLParserException { + StringValue standard = literal("'a\\nb'", Dialect.POSTGRESQL); + assertEquals(StringValue.Escaping.STANDARD, standard.getEscaping()); + assertEquals("a\\nb", standard.getNotExcapedValue()); + + StringValue escape = literal("E'it\\'s\\tx\\\\'", Dialect.POSTGRESQL); + assertEquals(StringValue.Escaping.POSTGRESQL_ESCAPE_STRING, escape.getEscaping()); + assertEquals("it's\tx\\", escape.getNotExcapedValue()); + assertEquals("it's", literal("E'it''s'", Dialect.POSTGRESQL).getNotExcapedValue()); + // octal and hexadecimal escapes are bytes of the UTF-8 encoding + assertEquals("AAéé café 😀 q", literal( + "E'\\x41\\101\\303\\251\\xC3\\xA9 caf\\u00e9 \\U0001F600 \\q'", + Dialect.POSTGRESQL).getNotExcapedValue()); + assertEquals("it''s\\n", literal("$$it''s\\n$$", Dialect.POSTGRESQL) + .getNotExcapedValue()); + } + + @Test + void testPostgresEscapeStringUnicodeEscapes() throws JSQLParserException { + String[][] cases = { + {"E'\\U0010FFFF'", new String(Character.toChars(0x10FFFF))}, + // a surrogate pair as two escapes, in either form + {"E'\\uD83D\\uDE00'", "\uD83D\uDE00"}, + {"E'\\ud83d\\U0000de00'", "\uD83D\uDE00"}, + {"E'\\x41\\uD83D\\uDE00\\101'", "A\uD83D\uDE00A"}}; + for (String[] c : cases) { + assertEquals(c[1], literal(c[0], Dialect.POSTGRESQL).getNotExcapedValue(), c[0]); + } + + // the escapes PostgreSQL rejects: too few digits, invalid code points and a surrogate + // not written as a pair + for (String sql : new String[] {"E'\\u00e'", "E'\\U0001F60'", "E'\\u0000'", + "E'\\U00000000'", "E'\\U00110000'", "E'\\U7FFFFFFF'", "E'\\UFFFFFFFF'", + "E'\\uD800'", "E'\\uD800x'", "E'\\uD800\\u0041'", "E'\\uD800\\x41'", + "E'\\uD800'''", "E'\\uDE00'", "E'\\uD800\\uD800\\uDE00'"}) { + assertThrows(IllegalArgumentException.class, () -> new StringValue(sql) + .withEscaping(StringValue.Escaping.POSTGRESQL_ESCAPE_STRING) + .getNotExcapedValue(), sql); + assertThrows(JSQLParserException.class, () -> literal(sql, Dialect.POSTGRESQL), sql); + } + // only the escapes of PostgreSQL strings are checked + assertEquals("\\uD800", literal("'\\uD800'", Dialect.POSTGRESQL).getNotExcapedValue()); + assertEquals("\\uD800", literal("E'\\uD800'", null).getNotExcapedValue()); + } + + @Test + void testPostgresUnicodeEscapeStrings() throws JSQLParserException { + StringValue value = literal("U&'\\0041BC \\00e9'", Dialect.POSTGRESQL); + assertEquals(StringValue.Escaping.POSTGRESQL_UNICODE_ESCAPE, value.getEscaping()); + assertEquals("U&", value.getPrefix()); + assertEquals("\\0041BC \\00e9", value.getValue()); + assertNull(value.getUnicodeEscapeCharacter()); + assertEquals("ABC \u00e9", value.getNotExcapedValue()); + + StringValue custom = literal("U&'d!0061t!+000061' UESCAPE '!'", Dialect.POSTGRESQL); + assertEquals("d!0061t!+000061", custom.getValue()); + assertEquals(Character.valueOf('!'), custom.getUnicodeEscapeCharacter()); + assertEquals("data", custom.getNotExcapedValue()); + assertEquals("U&'d!0061t!+000061' UESCAPE '!'", custom.toString()); + + String[][] cases = { + {"U&'\\+01F600'", "\uD83D\uDE00"}, + {"U&'a\\\\b'", "a\\b"}, + {"U&'it''s'", "it's"}, + // a surrogate pair as two escapes, in either form + {"u&'\\D83D\\DE00'", "\uD83D\uDE00"}, + {"U&'\\d83d\\+00de00'", "\uD83D\uDE00"}, + {"U&'caf\\00E9 \\+00AC00 \uD55C\uAE00'", "caf\u00e9 \uAC00 \uD55C\uAE00"}, + // a doubled escape character stands for itself; other backslashes stay + {"U&'a!!b\\n!0021' UESCAPE '!'", "a!b\\n!"}, + {"U&'\\' UESCAPE '!'", "\\"}, + {"U&'#0041' uescape '#'", "A"}, + {"U&'x' UESCAPE '\\'", "x"}, + {"U&''", ""}}; + for (String[] c : cases) { + assertEquals(c[1], literal(c[0], Dialect.POSTGRESQL).getNotExcapedValue(), c[0]); + } + } + + @Test + void testPostgresUnicodeEscapeStringsRoundTrip() throws JSQLParserException { + for (String sql : new String[] { + "COMMENT ON TABLE t IS U&'\\0041BC \\00e9'", + "COMMENT ON TABLE t IS U&'d!0061t!+000061' UESCAPE '!'", + "CREATE TABLE t (a text DEFAULT U&'\\+01F600', b text DEFAULT U&'it''s')", + "ALTER TABLE t ALTER COLUMN a SET DEFAULT U&'a\\\\b'", + "INSERT INTO t VALUES (U&'#0041' UESCAPE '#', U&'x')", + "SELECT * FROM t WHERE a = U&'\\00e9' AND b LIKE U&'%!0041' UESCAPE '!'"}) { + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + Statement reparsed = CCJSqlParserUtil.parse(statement.toString(), + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(sql, reparsed.toString()); + } + Comment comment = (Comment) CCJSqlParserUtil.parse( + "COMMENT ON TABLE t IS U&'d!0061t!+000061' UESCAPE '!'", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals("data", comment.getComment().getNotExcapedValue()); + // a continuation line belongs to the same Unicode escape string, as on the server + StringValue continued = literal("U&'\\0041'\n'\\0042'", Dialect.POSTGRESQL); + assertEquals("AB", continued.getNotExcapedValue()); + assertEquals("U&'\\0041\\0042'", continued.toString()); + } + + @Test + void testConstructedUnicodeEscapeStrings() { + StringValue value = new StringValue("U&'!0041' UESCAPE '!'"); + assertEquals("U&", value.getPrefix()); + assertEquals("!0041", value.getValue()); + assertEquals(StringValue.Escaping.POSTGRESQL_UNICODE_ESCAPE, value.getEscaping()); + assertEquals("A", value.getNotExcapedValue()); + assertEquals("U&'!0041' UESCAPE '!'", value.toString()); + assertEquals("U&'!0041'", value.withUnicodeEscapeCharacter(null).toString()); + assertEquals("!0041", value.getNotExcapedValue()); + + StringValue built = new StringValue("'#00e9'").withPrefix("U&") + .withEscaping(StringValue.Escaping.POSTGRESQL_UNICODE_ESCAPE) + .withUnicodeEscapeCharacter('#'); + assertEquals("U&'#00e9' UESCAPE '#'", built.toString()); + assertEquals("\u00e9", built.getNotExcapedValue()); + ExpressionDeParser deParser = new ExpressionDeParser(); + built.accept(deParser, null); + assertEquals("U&'#00e9' UESCAPE '#'", deParser.getBuilder().toString()); + + assertThrows(IllegalArgumentException.class, + () -> new StringValue("U&'\\00'").getNotExcapedValue()); + assertThrows(IllegalArgumentException.class, + () -> new StringValue("U&'x' UESCAPE '+'").getNotExcapedValue()); + // without the escaping a U& prefix only renders + assertEquals("\\0041", new StringValue("'\\0041'").withPrefix("U&").getNotExcapedValue()); + } + + @Test + void testUnicodeUnescapingIsLinear() { + int escapes = 500_000; + StringBuilder sb = new StringBuilder(13 * escapes + 4).append("U&'"); + for (int i = 0; i < escapes; i++) { + sb.append("\\0041\\+01F600"); + } + StringValue instance = new StringValue(sb.append('\'').toString()); + + String result = assertTimeoutPreemptively(Duration.ofSeconds(2), + instance::getNotExcapedValue); + + assertEquals(3 * escapes, result.length()); + } + + @Test + void testOtherBackslashDialectsKeepTheirSource() throws JSQLParserException { + for (Dialect dialect : new Dialect[] {Dialect.BIGQUERY, Dialect.DATABRICKS, + Dialect.SNOWFLAKE}) { + StringValue value = literal("'it\\'s'", dialect); + assertEquals(StringValue.Escaping.STANDARD, value.getEscaping()); + assertEquals("it\\'s", value.getNotExcapedValue()); + } + } + + @Test + void testConstructedValuesDefaultToStandardEscaping() { + StringValue value = new StringValue("'a\\n''b'"); + assertEquals(StringValue.Escaping.STANDARD, value.getEscaping()); + assertEquals("a\\n'b", value.getNotExcapedValue()); + assertEquals("a\n'b", + value.withEscaping(StringValue.Escaping.MYSQL_BACKSLASH).getNotExcapedValue()); + value.setEscaping(null); + assertEquals(StringValue.Escaping.STANDARD, value.getEscaping()); + } + + @Test + void testColumnDefaultValueIsUnescaped() throws JSQLParserException { + for (String literal : new String[] {"'it''s'", "'it\\'s'"}) { + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD COLUMN s VARCHAR(10) DEFAULT " + literal, + parser -> parser.withDialect(Dialect.MYSQL)); + ColumnOption option = alter.getAlterExpressions().get(0).getColDataTypeList().get(0) + .getColumnOptions().get(0); + assertEquals("it's", + ((StringValue) option.getDefaultExpression()).getNotExcapedValue()); + assertEquals("ALTER TABLE t ADD COLUMN s VARCHAR (10) DEFAULT " + literal, + alter.toString()); + } + } + + @Test + void testBackslashUnescapingIsLinear() { + int pairs = 1_000_000; + StringBuilder sb = new StringBuilder(2 * pairs + 2).append('\''); + for (int i = 0; i < pairs; i++) { + sb.append("\\'"); + } + StringValue instance = new StringValue(sb.append('\'').toString()) + .withEscaping(StringValue.Escaping.MYSQL_BACKSLASH); + + String result = assertTimeoutPreemptively(Duration.ofSeconds(2), + instance::getNotExcapedValue); + + assertEquals(pairs, result.length()); + } + @Test public void testPrefixes() { checkStringValue("E'test'", "test", "E"); diff --git a/src/test/java/net/sf/jsqlparser/expression/TaggedDollarStringTest.java b/src/test/java/net/sf/jsqlparser/expression/TaggedDollarStringTest.java new file mode 100644 index 0000000000..4dd3fa9398 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/TaggedDollarStringTest.java @@ -0,0 +1,190 @@ +/*- + * #%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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserConstants; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.StreamProvider; +import net.sf.jsqlparser.parser.Token; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class TaggedDollarStringTest { + static Stream tags() { + return Stream.of("", "tag", "Tag_123", "_", "한글", "étiquette"); + } + + @ParameterizedTest + @MethodSource("tags") + void preservesLiteralBodiesAndDelimiters(String tag) throws Exception { + String delimiter = "$" + tag + "$"; + for (String body : List.of("", "abc", "a\nb\r\nc\t ", "x 'one' ''two'' \\ end", + "/* comment */ -- more\n#hash", "[ {\"some\":\"json\",\"with\":\"properties$\"} ]", + "$1 $other$ こんにちは")) { + String literal = delimiter + body + delimiter; + String sql = "SELECT " + literal + " AS value, 2 FROM t"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + StringValue value = + assertInstanceOf(StringValue.class, select.getSelectItem(0).getExpression()); + assertEquals(body, value.getValue()); + assertEquals(body, value.getNotExcapedValue()); + assertEquals(delimiter, value.getQuoteStr()); + assertEquals(literal, value.toString()); + StringBuilder builder = new StringBuilder(); + select.accept(new StatementDeParser(builder), null); + PlainSelect again = (PlainSelect) CCJSqlParserUtil.parse(builder.toString(), + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(body, again.getSelectItem(0).getExpression(StringValue.class).getValue()); + assertEquals(select.toString(), builder.toString()); + } + } + + @Test + void keepsDifferentTagsAndDollarSignsInsideBody() throws Exception { + String body = "$other$ text $Tag$ $$ $1 $t"; + PlainSelect select = + (PlainSelect) CCJSqlParserUtil.parse("SELECT $tag$" + body + "$tag$::text, $1", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + CastExpression cast = select.getSelectItem(0).getExpression(CastExpression.class); + assertEquals(body, ((StringValue) cast.getLeftExpression()).getValue()); + assertInstanceOf(JdbcParameter.class, select.getSelectItem(1).getExpression()); + } + + @Test + void retainsIdentifiersWithPostgreSqlDialect() throws Exception { + PlainSelect select = (PlainSelect) CCJSqlParserUtil + .parse("SELECT $parameter, foo$bar, \"$tag$abc$tag$\", $1 FROM t", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + for (int i = 0; i < 3; i++) { + assertInstanceOf(Column.class, select.getSelectItem(i).getExpression()); + } + assertInstanceOf(JdbcParameter.class, select.getSelectItem(3).getExpression()); + } + + static Stream> identifierConfigurations() { + return Stream.concat( + Stream.>of(parser -> { + }, + parser -> parser.withDialect(Dialect.POSTGRESQL) + .withDollarQuotedStringTags(false)), + Stream.of(Dialect.values()).filter(dialect -> dialect != Dialect.POSTGRESQL) + .map(dialect -> parser -> parser.withDialect(dialect))); + } + + @ParameterizedTest + @MethodSource("identifierConfigurations") + void retainsIdentifiersAndUntaggedLiterals(Consumer configuration) + throws Exception { + for (String identifier : List.of("$tag$abc$tag$", "$tag$identifier")) { + PlainSelect select = + (PlainSelect) CCJSqlParserUtil.parse("SELECT " + identifier, configuration); + assertEquals(identifier, + select.getSelectItem(0).getExpression(Column.class).getColumnName()); + } + PlainSelect untagged = (PlainSelect) CCJSqlParserUtil.parse("SELECT $$text$$", + configuration); + assertEquals("text", untagged.getSelectItem(0).getExpression(StringValue.class).getValue()); + } + + @Test + void supportsExplicitOptInWithoutDialect() throws Exception { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT $tag$abc$tag$", + parser -> parser.withDollarQuotedStringTags(true)); + assertEquals("abc", select.getSelectItem(0).getExpression(StringValue.class).getValue()); + } + + @Test + void retainsBodyWithOtherLexerOptions() throws Exception { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT $t$#hash\n\\text't$tag$ \"q\"$t$", + parser -> parser.withDialect(Dialect.MYSQL).withDollarQuotedStringTags(true)); + assertEquals("#hash\n\\text't$tag$ \"q\"", + select.getSelectItem(0).getExpression(StringValue.class).getValue()); + } + + @Test + void keepsLineColumnAndAbsoluteTokenPositions() { + String literal = "$tag$a\nb$tag$"; + CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT " + literal + ", 2") + .withDialect(Dialect.POSTGRESQL); + parser.getNextToken(); + Token value = parser.getNextToken(); + Token comma = parser.getNextToken(); + assertEquals(CCJSqlParserConstants.S_CHAR_LITERAL, value.kind); + assertEquals(literal, value.image); + assertEquals(1, value.beginLine); + assertEquals(8, value.beginColumn); + assertEquals(2, value.endLine); + assertEquals(6, value.endColumn); + assertEquals(8, value.absoluteBegin); + assertEquals(8 + literal.length(), value.absoluteEnd); + assertEquals(value.absoluteEnd, comma.absoluteBegin); + assertEquals(7, comma.beginColumn); + } + + @Test + void recognizesFunctionBodyAndFollowingStatement() throws Exception { + String body = "SELECT 'a;''b'::text;\n"; + String sql = + "CREATE FUNCTION f() RETURNS text AS $fn$" + body + "$fn$ LANGUAGE SQL; SELECT 42;"; + Statements statements = CCJSqlParserUtil.parseStatements(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(2, statements.size()); + assertEquals("SELECT 42", statements.get(1).toString()); + org.junit.jupiter.api.Assertions + .assertTrue(statements.get(0).toString().contains("$fn$" + body + "$fn$")); + assertEquals(2, CCJSqlParserUtil.parseStatements(statements.toString(), + parser -> parser.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + @Timeout(10) + void handlesLongBodiesAndOverlappingDelimiterPrefixes() throws Exception { + String body = "$ta$tagX $tagtagX\n".repeat(12000); + String sql = "SELECT $tagtag$" + body + "$tagtag$"; + PlainSelect select = + (PlainSelect) new CCJSqlParser(new StreamProvider(new StringReader(sql))) + .withDialect(Dialect.POSTGRESQL).Statement(); + assertEquals(body, select.getSelectItem(0).getExpression(StringValue.class).getValue()); + PlainSelect streamed = (PlainSelect) CCJSqlParserUtil.newParser( + new java.io.ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8)), "UTF-8") + .withDialect(Dialect.POSTGRESQL).Statement(); + assertEquals(body, streamed.getSelectItem(0).getExpression(StringValue.class).getValue()); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT $tag$missing", "SELECT $Tag$wrong$tag$", "SELECT $t$ends$t", + "SELECT $a$text$b$", "SELECT $$missing"}) + void rejectsUnterminatedOrMismatchedTags(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL).withTimeOut(1000))); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java new file mode 100644 index 0000000000..38f20111df --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java @@ -0,0 +1,193 @@ +/*- + * #%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.JSQLParserException; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.ComparisonOperator; +import net.sf.jsqlparser.expression.operators.relational.JsonOperator; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +/** + * Tests for the ClickHouse-style ternary conditional operator {@code cond ? then : else}. + */ +class TernaryExpressionTest { + + @Test + void testIssue2436() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT x > 0 ? 'y' : 'n' FROM t", true); + + Select select = (Select) CCJSqlParserUtil.parse("SELECT x > 0 ? 'y' : 'n' FROM t"); + TernaryExpression ternary = (TernaryExpression) ((PlainSelect) select).getSelectItem(0) + .getExpression(); + Assertions.assertTrue(ternary.getCondition() instanceof ComparisonOperator); + Assertions.assertTrue(ternary.getThenExpression() instanceof StringValue); + Assertions.assertTrue(ternary.getElseExpression() instanceof StringValue); + } + + @ParameterizedTest + @ValueSource(strings = { + // select items, where, order by, group by, having + "SELECT a ? b : c FROM t", + "SELECT * FROM t WHERE a ? b : c", + "SELECT * FROM t WHERE a ? b : c ORDER BY x ? y : z", + "SELECT id FROM t GROUP BY v ? 1 : 0 HAVING count(*) > 1", + // function arguments, nested in parentheses and subqueries + "SELECT abs(a ? b : c) FROM t", + "SELECT (a ? b : c) FROM t", + "SELECT * FROM (SELECT a ? b : c AS v FROM t) x", + // dml statements + "UPDATE t SET v = a ? b : c", + "INSERT INTO t VALUES (a ? b : c)", + "DELETE FROM t WHERE a ? b : c", + // joins + "SELECT * FROM t1 JOIN t2 ON t1.a ? t1.b : t2.c", + // branches with arithmetic or boolean operators + "SELECT x > 0 ? 1 + 2 : 3 * 4 FROM t", + "SELECT a ? b OR c : d AND e FROM t", + "SELECT a IS NULL ? 'x' : y FROM t", + // jdbc parameters as branches + "SELECT a ? ? : c FROM t", + "SELECT a ? b + ? : c FROM t", + "SELECT a ? ? : ? FROM t", + "SELECT a ? b : ? FROM t", + "SELECT * FROM t WHERE x = ? AND y ? z : w", + // case expression inside a branch + "SELECT a ? CASE WHEN b THEN c ELSE d END : e FROM t", + // cast target type ends the then-branch (a bare ":" after a type + // keyword is the ternary separator, not a named parameter) + "SELECT a ? b :: int : c FROM t", + "SELECT x > 0 ? CAST(b AS int) : c FROM t" + }) + void testTernaryInVariousContexts(String sqlStr) throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPrecedenceBindsLooserThanBooleanOperators() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("SELECT * FROM t WHERE a OR b ? c : d"); + Expression where = ((PlainSelect) select).getWhere(); + Assertions.assertTrue(where instanceof TernaryExpression); + Assertions.assertTrue(((TernaryExpression) where).getCondition() instanceof OrExpression); + + select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE a AND b ? c : d"); + where = ((PlainSelect) select).getWhere(); + Assertions.assertTrue(where instanceof TernaryExpression); + Assertions.assertTrue(((TernaryExpression) where).getCondition() instanceof AndExpression); + + select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE a ? b : c OR d"); + where = ((PlainSelect) select).getWhere(); + Assertions.assertTrue(where instanceof TernaryExpression); + Assertions.assertTrue( + ((TernaryExpression) where).getElseExpression() instanceof OrExpression); + } + + @Test + void testRightAssociativeNesting() throws JSQLParserException { + // a ? b : c ? d : e ==> a ? b : (c ? d : e) + Select select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE a ? b : c ? d : e"); + TernaryExpression ternary = (TernaryExpression) ((PlainSelect) select).getWhere(); + Assertions.assertTrue(ternary.getThenExpression() instanceof Column); + Assertions.assertTrue(ternary.getElseExpression() instanceof TernaryExpression); + + // a ? b ? c : d : e ==> a ? (b ? c : d) : e + select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE a ? b ? c : d : e"); + ternary = (TernaryExpression) ((PlainSelect) select).getWhere(); + Assertions.assertTrue(ternary.getThenExpression() instanceof TernaryExpression); + Assertions.assertTrue(ternary.getElseExpression() instanceof Column); + } + + @ParameterizedTest + @ValueSource(strings = { + // PostgreSQL JSON operators must keep working + "SELECT col ? 'key' FROM t", + "SELECT col ?| 'key' FROM t", + "SELECT col ?& 'key' FROM t", + "SELECT * FROM t WHERE col ? 'key' AND x = 1", + // JSON path operator must keep working + "SELECT col -> 'key' FROM t", + // JDBC parameters must keep working + "SELECT * FROM t WHERE x = ?", + "SELECT * FROM t WHERE x = ?5", + "SELECT * FROM t WHERE x = ? AND y = ?", + "SELECT * FROM t LIMIT ?", + // array ranges and casts are unaffected + "SELECT ARRAY[1:3]", + "SELECT CAST(a AS CHAR)" + }) + void testUnrelatedSyntaxUnaffected(String sqlStr) throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testJsonbOperatorWithNamedParameter() throws JSQLParserException { + // regression guard (ternary support, #2466): a ":" in operand position + // starts a JDBC named parameter and must not close a ternary then-branch + Select select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE j ? :key"); + Expression where = ((PlainSelect) select).getWhere(); + Assertions.assertTrue(where instanceof JsonOperator); + Assertions.assertTrue( + ((JsonOperator) where).getRightExpression() instanceof JdbcNamedParameter); + } + + @Test + void testTernaryWithCastThenBranch() throws JSQLParserException { + // regression guard: a DATA_TYPE token can end the then-branch, so the + // ":" after the cast target type must close the ternary + Select select = (Select) CCJSqlParserUtil.parse("SELECT a ? b :: int : c FROM t"); + TernaryExpression ternary = (TernaryExpression) ((PlainSelect) select).getSelectItem(0) + .getExpression(); + Assertions.assertTrue(ternary.getThenExpression() instanceof CastExpression); + } + + @Test + void testTernaryWithPositionalParameterThenBranch() throws JSQLParserException { + // regression guard: a bare "?" positional parameter is a complete + // expression, so the ":" after it closes the ternary instead of being + // skipped in favor of the jsonb reading + Select select = (Select) CCJSqlParserUtil.parse("SELECT a ? ? : c FROM t"); + TernaryExpression ternary = (TernaryExpression) ((PlainSelect) select).getSelectItem(0) + .getExpression(); + Assertions.assertTrue(ternary.getThenExpression() instanceof JdbcParameter); + Assertions.assertTrue(ternary.getElseExpression() instanceof Column); + + // a positional parameter as the LAST token of the then-branch, too + select = (Select) CCJSqlParserUtil.parse("SELECT a ? b + ? : c FROM t"); + ternary = (TernaryExpression) ((PlainSelect) select).getSelectItem(0).getExpression(); + Assertions.assertTrue(ternary.getThenExpression() instanceof Addition); + } + + @ParameterizedTest + @ValueSource(strings = { + // PostgreSQL jsonb operator combined with JDBC named parameters + "SELECT * FROM t WHERE j ? :key", + "SELECT * FROM t WHERE j ? 'k' AND x = :p", + "SELECT * FROM t WHERE j ? 'k' OR x = :p", + "SELECT * FROM t WHERE j ? :key AND x = 1", + // named parameters keep working inside ternary branches, too + "SELECT a ? :t : :e FROM t", + "SELECT * FROM t WHERE c = :c AND a ? :t : :e" + }) + void testTernaryAndJsonbWithJdbcNamedParameters(String sqlStr) throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/XMLSerializeExprTest.java b/src/test/java/net/sf/jsqlparser/expression/XMLSerializeExprTest.java new file mode 100644 index 0000000000..f3c6ea36c2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/XMLSerializeExprTest.java @@ -0,0 +1,151 @@ +/*- + * #%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 static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.OrderByElement; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.OrderByDeParser; +import net.sf.jsqlparser.util.validation.validator.ExpressionValidator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class XMLSerializeExprTest { + @ParameterizedTest + @ValueSource(strings = { + "SELECT XMLSERIALIZE(CONTENT XMLTYPE('Grandco')) AS xmlserialize_doc FROM DUAL", + "SELECT XMLSERIALIZE(DOCUMENT payload AS CLOB) FROM docs", + "SELECT XMLSERIALIZE(CONTENT payload AS VARCHAR2(4000)) FROM docs", + "SELECT XMLSERIALIZE(CONTENT payload AS BLOB ENCODING 'UTF-8' VERSION '1.0' INDENT SIZE = 2 SHOW DEFAULTS) FROM docs", + "SELECT XMLSERIALIZE(DOCUMENT payload NO INDENT HIDE DEFAULTS) FROM docs", + "SELECT XMLSERIALIZE(CONTENT payload INDENT) FROM docs", + "SELECT XMLSERIALIZE(CONTENT payload INDENT SIZE = 0) FROM docs", + "SELECT XMLSERIALIZE(CONTENT XMLAGG(XMLELEMENT(NAME e, payload) ORDER BY id) AS CLOB) FROM docs", + "SELECT XMLSERIALIZE(CONTENT (SELECT payload FROM docs)) FROM DUAL", + "SELECT XMLSERIALIZE(CONTENT XMLTYPE('it''s XML') VERSION '1.1') FROM DUAL" + }) + void roundTripOracleSyntax(String sql) throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + } + + @ParameterizedTest + @ValueSource(strings = { + "XMLSERIALIZE(CONTENT)", + "XMLSERIALIZE(DOCUMENT x AS)", + "XMLSERIALIZE(CONTENT x NO INDENT SIZE = 2)", + "XMLSERIALIZE(CONTENT x INDENT SIZE = -1)", + "XMLSERIALIZE(CONTENT x ENCODING UTF8)", + "XMLSERIALIZE(CONTENT x VERSION 1)", + "XMLSERIALIZE(CONTENT x SHOW)", + "XMLSERIALIZE(CONTENT x NO SOMETHING)", + "XMLSERIALIZE(CONTENT x INDENT INDENT)" + }) + void rejectMalformedSyntax(String expression) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parseExpression(expression)); + } + + @Test + void inspectOptionsAndOmission() throws JSQLParserException { + XMLSerializeExpr xml = (XMLSerializeExpr) CCJSqlParserUtil.parseExpression( + "XMLSERIALIZE(CONTENT payload ENCODING 'UTF-8' VERSION '1.0' INDENT SIZE = 4 SHOW DEFAULTS)"); + assertEquals(XMLSerializeExpr.SerializationMode.CONTENT, xml.getSerializationMode()); + assertNull(xml.getDataType()); + assertEquals("UTF-8", xml.getEncoding().getValue()); + assertEquals("1.0", xml.getVersion().getValue()); + assertEquals(Boolean.TRUE, xml.getIndent()); + assertEquals(4, xml.getIndentSize().getValue()); + assertEquals(Boolean.TRUE, xml.getShowDefaults()); + assertEquals(4, xml.getExpressions().size()); + XMLSerializeExpr omitted = (XMLSerializeExpr) CCJSqlParserUtil.parseExpression( + "XMLSERIALIZE(DOCUMENT payload)"); + assertNull(omitted.getIndent()); + assertNull(omitted.getShowDefaults()); + omitted.setIndentSize(new LongValue(2)); + assertThrows(IllegalArgumentException.class, omitted::validateOptions); + } + + @Test + void legacyVisitorsHandleAbsentOrderBy() throws JSQLParserException { + XMLSerializeExpr xml = (XMLSerializeExpr) CCJSqlParserUtil.parseExpression( + "XMLSERIALIZE(XMLAGG(XMLTEXT(payload)) AS VARCHAR(100))"); + assertNull(xml.getSerializationMode()); + assertNull(xml.getOrderByElements()); + List seen = new ArrayList<>(); + xml.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + seen.add(column.getColumnName() + ":" + context); + return null; + } + }, "context"); + assertEquals(List.of("payload:context"), seen); + } + + @Test + void visitAndRewriteLegacyOrderByWithContext() throws JSQLParserException { + XMLSerializeExpr xml = (XMLSerializeExpr) CCJSqlParserUtil.parseExpression( + "XMLSERIALIZE(XMLAGG(XMLTEXT(payload) ORDER BY id DESC NULLS LAST) AS VARCHAR(100))"); + StringBuilder sql = new StringBuilder(); + ExpressionDeParser writer = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + assertEquals("context", context); + return getBuilder().append("new_").append(column.getColumnName()); + } + }; + writer.setBuilder(sql); + xml.accept(writer, "context"); + assertEquals( + "xmlserialize(xmlagg(xmltext(new_payload) ORDER BY new_id DESC NULLS LAST) AS VARCHAR (100))", + sql.toString()); + List seen = new ArrayList<>(); + xml.accept(new ExpressionValidator() { + @Override + public Void visit(Column column, S context) { + seen.add(column.getColumnName()); + assertEquals("context", context); + return null; + } + }, "context"); + assertEquals(List.of("payload", "id"), seen); + } + + @Test + void discoverTablesInSerializedSubquery() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT XMLSERIALIZE(CONTENT (SELECT payload FROM docs)) FROM outer_source"); + assertTrue( + new TablesNamesFinder().getTableList((net.sf.jsqlparser.statement.Statement) select) + .containsAll(List.of("docs", "outer_source"))); + } + + @Test + void preserveLegacyOrderByCustomization() { + StringBuilder sql = new StringBuilder(); + OrderByDeParser writer = new OrderByDeParser(new ExpressionDeParser(), sql) { + @Override + public void deParseElement(OrderByElement element) { + getBuilder().append("custom_order"); + } + }; + writer.deParse(false, List.of(new OrderByElement().withExpression(new Column("id")))); + assertEquals(" ORDER BY custom_order", sql.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/operators/relational/BetweenTest.java b/src/test/java/net/sf/jsqlparser/expression/operators/relational/BetweenTest.java index 4fd8fd706a..ee04111bc4 100644 --- a/src/test/java/net/sf/jsqlparser/expression/operators/relational/BetweenTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/operators/relational/BetweenTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression.operators.relational; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; @@ -50,4 +51,78 @@ void testBetweenASymmetricIssue2250() throws JSQLParserException { Assertions.assertFalse(between.isUsingSymmetric()); Assertions.assertTrue(between.isUsingAsymmetric()); } + + @Test + void testBetweenWithOldOracleJoinSyntaxOnBothOperandsIssue672() throws JSQLParserException { + String sqlStr = + "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 BETWEEN t2.col2(+) AND t2.col3(+)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Between between = (Between) select.getWhere(); + + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) between.getBetweenExpressionStart()).getOldOracleJoinSyntax()); + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) between.getBetweenExpressionEnd()).getOldOracleJoinSyntax()); + } + + @Test + void testBetweenWithOldOracleJoinSyntaxOnStartOperandIssue672() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 BETWEEN t2.col2(+) AND 5", true); + } + + @Test + void testBetweenWithOldOracleJoinSyntaxOnEndOperandIssue672() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 BETWEEN 1 AND t2.col3(+)", true); + } + + @Test + void testNotBetweenWithOldOracleJoinSyntaxIssue672() throws JSQLParserException { + String sqlStr = + "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 NOT BETWEEN t2.col2(+) AND t2.col3(+)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Between between = (Between) select.getWhere(); + + assertTrue(between.isNot()); + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) between.getBetweenExpressionStart()).getOldOracleJoinSyntax()); + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) between.getBetweenExpressionEnd()).getOldOracleJoinSyntax()); + } + + @Test + void testBetweenSymmetricWithOldOracleJoinSyntaxIssue672() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t1, t2 WHERE t1.c BETWEEN SYMMETRIC t2.c(+) AND t2.d(+)", true); + } + + @Test + void testBetweenWithOldOracleJoinSyntaxInJoinOnClauseIssue672() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t1 JOIN t2 ON t1.c BETWEEN t2.c(+) AND t2.d(+)", true); + } + + @Test + void testBetweenWithOldOracleJoinSyntaxOnLeftOperand() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM table1 t1, table2 t2 WHERE t2.col2(+) BETWEEN t1.col1 AND t1.col3", + true); + } + + @Test + void testBetweenWithOldOracleJoinSyntaxBeforeComparisonSuffixOnOperand() + throws JSQLParserException { + // (+) directly followed by a comparison operator keeps the pre-existing + // RegularConditionRHS path, where the marker sits on the comparison itself + String sqlStr = + "SELECT * FROM t1, t2 WHERE t1.c BETWEEN t2.c(+) = 5 AND 1"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Between between = (Between) select.getWhere(); + EqualsTo comparison = (EqualsTo) between.getBetweenExpressionStart(); + + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, comparison.getOldOracleJoinSyntax()); + assertEquals(EqualsTo.NO_ORACLE_JOIN, + ((Column) comparison.getLeftExpression()).getOldOracleJoinSyntax()); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/operators/relational/ComparisonOperatorTest.java b/src/test/java/net/sf/jsqlparser/expression/operators/relational/ComparisonOperatorTest.java index 7be79658ab..4530364195 100644 --- a/src/test/java/net/sf/jsqlparser/expression/operators/relational/ComparisonOperatorTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/operators/relational/ComparisonOperatorTest.java @@ -10,10 +10,15 @@ package net.sf.jsqlparser.expression.operators.relational; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class ComparisonOperatorTest { @@ -43,4 +48,95 @@ void testCosineSimilarity() throws JSQLParserException { Assertions.assertInstanceOf(CosineSimilarity.class, CCJSqlParserUtil.parseExpression("embedding <=> '[3,1,2]'")); } + + @Test + void testVectorDistanceOperatorsInProjection() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a <-> b FROM t"); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a <#> b FROM t"); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a <=> b FROM t"); + Assertions.assertInstanceOf(GeometryDistance.class, + CCJSqlParserUtil.parseExpression("a <-> b")); + Assertions.assertInstanceOf(GeometryDistance.class, + CCJSqlParserUtil.parseExpression("a <#> b")); + Assertions.assertInstanceOf(CosineSimilarity.class, + CCJSqlParserUtil.parseExpression("a <=> b")); + } + + @ParameterizedTest + @ValueSource(strings = {"<->", "<#>", "<=>"}) + void testVectorDistanceOperatorsFollowedByComparison(String distance) + throws JSQLParserException { + String[] comparators = {"<", ">", "="}; + for (String comparator : comparators) { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE a " + distance + " b " + comparator + " c"); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE (a " + distance + " b) " + comparator + " c"); + } + Assertions.assertInstanceOf(MinorThan.class, + CCJSqlParserUtil.parseExpression("a " + distance + " b < c")); + } + + @ParameterizedTest + @ValueSource(strings = {"<->", "<#>", "<=>"}) + void testVectorDistanceOperatorsInProjectionFollowedByComparison(String distance) + throws JSQLParserException { + String[] comparators = {"<", ">", "="}; + for (String comparator : comparators) { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a " + distance + " b " + comparator + " c FROM t"); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT (a " + distance + " b) " + comparator + " c FROM t"); + } + } + + @ParameterizedTest + @ValueSource(strings = {"<->", "<#>", "<=>"}) + void testVectorDistanceOperatorsShareBitwisePrecedence(String distance) + throws JSQLParserException { + Class distanceType = + "<=>".equals(distance) ? CosineSimilarity.class : GeometryDistance.class; + + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a " + distance + " b & c FROM t"); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a & b " + distance + " c FROM t"); + + BinaryExpression bitwiseRoot = + (BinaryExpression) CCJSqlParserUtil.parseExpression("a " + distance + " b & c"); + Assertions.assertInstanceOf(BitwiseAnd.class, bitwiseRoot); + Assertions.assertInstanceOf(distanceType, bitwiseRoot.getLeftExpression()); + + BinaryExpression distanceRoot = + (BinaryExpression) CCJSqlParserUtil.parseExpression("a & b " + distance + " c"); + Assertions.assertInstanceOf(distanceType, distanceRoot); + Assertions.assertInstanceOf(BitwiseAnd.class, distanceRoot.getLeftExpression()); + } + + @Test + void testGeometryDistanceOrderByControl() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t ORDER BY a <-> b"); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t ORDER BY a <#> b"); + } + + @Test + void testVectorDistancePrecedenceAboveComparisonBelowArithmetic() throws JSQLParserException { + BinaryExpression geometryComparison = Assertions.assertInstanceOf(MinorThan.class, + CCJSqlParserUtil.parseExpression("a <-> b + c < 5")); + GeometryDistance geometryDistance = Assertions.assertInstanceOf(GeometryDistance.class, + geometryComparison.getLeftExpression()); + Assertions.assertInstanceOf(Addition.class, geometryDistance.getRightExpression()); + + BinaryExpression cosineComparison = Assertions.assertInstanceOf(MinorThan.class, + CCJSqlParserUtil.parseExpression("a <=> b + c < 5")); + CosineSimilarity cosineSimilarity = Assertions.assertInstanceOf(CosineSimilarity.class, + cosineComparison.getLeftExpression()); + Assertions.assertInstanceOf(Addition.class, cosineSimilarity.getRightExpression()); + } + + @Test + void testComparisonChainsRemainInvalid() { + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE a < b < c")); + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE a <-> b < c < d")); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/operators/relational/FullTextSearchExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/operators/relational/FullTextSearchExpressionTest.java index 8a1c61714c..ee39216196 100644 --- a/src/test/java/net/sf/jsqlparser/expression/operators/relational/FullTextSearchExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/operators/relational/FullTextSearchExpressionTest.java @@ -9,7 +9,15 @@ */ package net.sf.jsqlparser.expression.operators.relational; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -35,4 +43,18 @@ public void testIssue1223() throws JSQLParserException { + "full_text desc", true); } + + @Test + public void testToStringIsLinearInMatchColumns() { + List columns = new ArrayList<>(); + for (int i = 0; i < 200_000; i++) { + columns.add(new Column("c" + i)); + } + FullTextSearch fullTextSearch = new FullTextSearch() + .withMatchColumns(new ExpressionList<>(columns)) + .withAgainstValue(new StringValue("x")); + + String sql = assertTimeoutPreemptively(Duration.ofSeconds(2), fullTextSearch::toString); + assertThat(sql).startsWith("MATCH (c0,c1,").endsWith(",c199999) AGAINST ('x')"); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/operators/relational/IsJsonExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/operators/relational/IsJsonExpressionTest.java new file mode 100644 index 0000000000..cf11094035 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/operators/relational/IsJsonExpressionTest.java @@ -0,0 +1,119 @@ +/*- + * #%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.operators.relational; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class IsJsonExpressionTest { + @ParameterizedTest + @ValueSource(strings = {"", " VALUE", " SCALAR", " ARRAY", " OBJECT", + " WITH UNIQUE KEYS", " WITHOUT UNIQUE", " OBJECT WITH UNIQUE", + " ARRAY WITHOUT UNIQUE KEYS"}) + void modelsOptionalTypeAndUniquenessInBothParserModes(String options) throws Exception { + for (boolean complex : List.of(false, true)) { + for (String not : List.of("", " NOT")) { + String sql = "SELECT payload IS" + not + " JSON" + options + " FROM events"; + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.POSTGRESQL).withAllowComplexParsing(complex)); + IsJsonExpression predicate = assertInstanceOf(IsJsonExpression.class, + select.getSelectItem(0).getExpression()); + assertEquals(!not.isEmpty(), predicate.isNot()); + assertEquals(sql, select.toString()); + roundTrip(select); + } + } + } + + @Test + void exposesMutableOptionsAndVisitsTheOperandWithContext() throws Exception { + PlainSelect select = parse("SELECT '{\"a\":1}' IS JSON"); + IsJsonExpression predicate = (IsJsonExpression) select.getSelectItem(0).getExpression(); + assertNull(predicate.getType()); + assertNull(predicate.getUniqueKeys()); + predicate.setType(IsJsonExpression.Type.OBJECT); + predicate.setUniqueKeys(IsJsonExpression.UniqueKeys.WITH); + predicate.setNot(true); + predicate.setLeftExpression(new StringValue("{}")); + assertEquals("SELECT '{}' IS NOT JSON OBJECT WITH UNIQUE KEYS", select.toString()); + roundTrip(select); + Object expected = new Object(); + List seen = new ArrayList<>(); + predicate.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + assertSame(expected, context); + seen.add(value.getValue()); + return null; + } + }, expected); + assertEquals(List.of("{}"), seen); + StringBuilder output = new StringBuilder(); + ExpressionDeParser visitor = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + assertSame(expected, context); + return getBuilder().append("'[]'"); + } + }; + visitor.setBuilder(output); + predicate.accept(visitor, expected); + assertEquals("'[]' IS NOT JSON OBJECT WITH UNIQUE KEYS", output.toString()); + } + + @Test + void traversesSubqueriesAndValidatesOperandFeatures() throws Exception { + PlainSelect select = parse("SELECT 1 FROM t WHERE (SELECT payload FROM hidden) IS JSON"); + assertThat(new TablesNamesFinder().getTables((Statement) select)).containsExactlyInAnyOrder( + "t", + "hidden"); + FeaturesAllowed allowed = + new FeaturesAllowed().add(FeaturesAllowed.DML).remove(Feature.jdbcParameter); + assertFalse( + new Validation(List.of(allowed), "SELECT 1 WHERE ? IS JSON").validate().isEmpty()); + roundTrip(parse("SELECT NULL IS JSON OR '{}' IS NOT JSON AND '1' IS JSON SCALAR")); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT x IS JSON WITH", "SELECT x IS JSON WITH KEYS", + "SELECT x IS JSON WITH UNIQUE KEYS WITHOUT UNIQUE", + "SELECT x IS NOT JSON WITH UNIQUE,"}) + void rejectsIncompleteOptions(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static PlainSelect parse(String sql) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(PlainSelect select) throws Exception { + StringBuilder out = new StringBuilder(); + select.accept(new StatementDeParser(out), null); + assertEquals(select.toString(), out.toString()); + assertEquals(select.toString(), parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/operators/relational/LikeExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/operators/relational/LikeExpressionTest.java index eb11e86e20..f06dc9e328 100644 --- a/src/test/java/net/sf/jsqlparser/expression/operators/relational/LikeExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/operators/relational/LikeExpressionTest.java @@ -12,9 +12,13 @@ import static org.junit.jupiter.api.Assertions.*; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.BinaryExpression; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -98,4 +102,87 @@ public void testMatchRegexp() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed( "select * from dual where v NOT MATCH_REGEXP 'keyword1 keyword2'", true); } + + @Test + public void testLikeWithOldOracleJoinSyntaxOnRightOperand() throws JSQLParserException { + String sqlStr = "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 LIKE t2.col2(+)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + LikeExpression like = (LikeExpression) select.getWhere(); + + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) like.getRightExpression()).getOldOracleJoinSyntax()); + assertEquals(EqualsTo.NO_ORACLE_JOIN, + ((Column) like.getLeftExpression()).getOldOracleJoinSyntax()); + } + + @Test + public void testNotLikeWithOldOracleJoinSyntaxOnRightOperand() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 NOT LIKE t2.col2(+)", true); + } + + @Test + public void testSimilarToWithOldOracleJoinSyntaxOnRightOperand() throws JSQLParserException { + String sqlStr = "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 SIMILAR TO t2.col2(+)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + BinaryExpression similarTo = (BinaryExpression) select.getWhere(); + + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) similarTo.getRightExpression()).getOldOracleJoinSyntax()); + } + + @Test + public void testSimilarToOnSeparateTokensWithOldOracleJoinSyntaxOnRightOperand() + throws JSQLParserException { + String sqlStr = "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 SIMILAR\nTO t2.col2(+)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + assertTrue(select.getWhere() instanceof SimilarToExpression); + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) ((SimilarToExpression) select.getWhere()).getRightExpression()) + .getOldOracleJoinSyntax()); + } + + @Test + public void testILikeWithOldOracleJoinSyntaxOnRightOperand() throws JSQLParserException { + String sqlStr = "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 ILIKE t2.col2(+)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + LikeExpression like = (LikeExpression) select.getWhere(); + + assertEquals(LikeExpression.KeyWord.ILIKE, like.getLikeKeyWord()); + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) like.getRightExpression()).getOldOracleJoinSyntax()); + } + + @Test + public void testLikeWithOldOracleJoinSyntaxInJoinOnClause() throws JSQLParserException { + String sqlStr = + "SELECT * FROM table1 t1 LEFT JOIN table2 t2 ON t1.a = t2.b AND t1.col1 LIKE t2.col2(+)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + LikeExpression like = (LikeExpression) ((AndExpression) select.getJoins().get(0) + .getOnExpression()).getRightExpression(); + + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) like.getRightExpression()).getOldOracleJoinSyntax()); + } + + @Test + public void testSimilarToWithOldOracleJoinSyntaxOnRightOperandAndEscape() + throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1 SIMILAR TO t2.col2(+) ESCAPE '\\'", + true); + } + + @Test + public void testLikeWithOldOracleJoinSyntaxOnLeftOperand() throws JSQLParserException { + String sqlStr = "SELECT * FROM table1 t1, table2 t2 WHERE t1.col1(+) LIKE t2.col2"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + LikeExpression like = (LikeExpression) select.getWhere(); + + assertEquals(EqualsTo.ORACLE_JOIN_RIGHT, + ((Column) like.getLeftExpression()).getOldOracleJoinSyntax()); + assertEquals(EqualsTo.NO_ORACLE_JOIN, + ((Column) like.getRightExpression()).getOldOracleJoinSyntax()); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/operators/relational/OraclePriorPositionTest.java b/src/test/java/net/sf/jsqlparser/expression/operators/relational/OraclePriorPositionTest.java new file mode 100644 index 0000000000..643342b71b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/operators/relational/OraclePriorPositionTest.java @@ -0,0 +1,211 @@ +/*- + * #%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.operators.relational; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; + +public class OraclePriorPositionTest { + + private static Expression whereOf(String sqlStr) throws JSQLParserException { + PlainSelect select = + (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + return select.getWhere(); + } + + @Test + public void testPriorBetween() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a BETWEEN 1 AND 2"); + Between between = (Between) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + between.getOraclePriorPosition()); + } + + @Test + public void testPriorLike() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a LIKE 'x%'"); + LikeExpression like = (LikeExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + like.getOraclePriorPosition()); + } + + @Test + public void testPriorNotLike() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a NOT LIKE 'x%'"); + LikeExpression like = (LikeExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + like.getOraclePriorPosition()); + } + + @Test + public void testPriorSimilarTo() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a SIMILAR TO 'x'"); + LikeExpression similarTo = (LikeExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + similarTo.getOraclePriorPosition()); + } + + @Test + public void testPriorSimilarToOnSeparateTokens() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a SIMILAR\nTO 'x'"); + SimilarToExpression similarTo = (SimilarToExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + similarTo.getOraclePriorPosition()); + } + + @Test + public void testPriorIsNull() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a IS NULL"); + IsNullExpression isNull = (IsNullExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + isNull.getOraclePriorPosition()); + } + + @Test + public void testPriorIsNotNull() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a IS NOT NULL"); + IsNullExpression isNull = (IsNullExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + isNull.getOraclePriorPosition()); + } + + @Test + public void testPriorIsTrue() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a IS TRUE"); + IsBooleanExpression isBoolean = (IsBooleanExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + isBoolean.getOraclePriorPosition()); + } + + @Test + public void testPriorIsUnknown() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a IS UNKNOWN"); + IsUnknownExpression isUnknown = (IsUnknownExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + isUnknown.getOraclePriorPosition()); + } + + @Test + public void testPriorIsDistinctFrom() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a IS DISTINCT FROM b"); + IsDistinctExpression isDistinct = (IsDistinctExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + isDistinct.getOraclePriorPosition()); + } + + @Test + public void testPriorMemberOf() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a MEMBER OF (1)"); + MemberOfExpression memberOf = (MemberOfExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + memberOf.getOraclePriorPosition()); + } + + @Test + public void testPriorIn() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a IN (1, 2)"); + InExpression in = (InExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + in.getOraclePriorPosition()); + } + + @Test + public void testPriorNotIn() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE PRIOR a NOT IN (1, 2)", + true); + } + + @Test + public void testPriorOnComparisonStartIsDeparsed() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE PRIOR a = b", true); + } + + @Test + public void testPriorOnComparisonEndIsDeparsed() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a = PRIOR b", true); + } + + @Test + public void testPriorIsNullShorthand() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a ISNULL"); + IsNullExpression isNull = (IsNullExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + isNull.getOraclePriorPosition()); + } + + @Test + public void testPriorNotNullShorthand() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE PRIOR a NOTNULL", true); + } + + @Test + public void testPriorIsFalse() throws JSQLParserException { + Expression where = whereOf("SELECT * FROM t WHERE PRIOR a IS FALSE"); + IsBooleanExpression isBoolean = (IsBooleanExpression) where; + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + isBoolean.getOraclePriorPosition()); + } + + @Test + public void testPriorIsNotUnknown() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE PRIOR a IS NOT UNKNOWN", + true); + } + + @Test + public void testPriorNotSimilarTo() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE PRIOR a NOT SIMILAR TO 'x'", + true); + } + + @Test + public void testPriorInJoinOnClause() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t JOIN t2 ON PRIOR t.c = t2.c", true); + } + + @Test + public void testPriorBetweenEndOperandStaysIntact() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a BETWEEN 1 AND PRIOR 2", + true); + } + + @Test + public void testPlainConditionsWithoutPriorAreUnchanged() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a = b", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a BETWEEN 1 AND 2", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a LIKE 'x%'", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a IS NULL", true); + } + + @Test + public void testNotPriorBetween() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE NOT PRIOR a BETWEEN 1 AND 2", true); + } + + @Test + public void testPriorOnSeveralConditions() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE PRIOR a = b AND PRIOR c BETWEEN 1 AND 2", true); + } + + @Test + public void testPriorCombinedWithOracleJoinOnBetween() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t1, t2 WHERE PRIOR t2.c(+) BETWEEN 1 AND 2", true); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java new file mode 100644 index 0000000000..6ce2622099 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java @@ -0,0 +1,377 @@ +/*- + * #%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.parser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.AnalyticExpression; +import net.sf.jsqlparser.expression.CaseExpression; +import net.sf.jsqlparser.expression.CastExpression; +import net.sf.jsqlparser.expression.ConnectByPriorOperator; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.sequence.CreateSequence; +import net.sf.jsqlparser.statement.select.PlainSelect; +import org.junit.jupiter.api.Test; + +/** + * Every object a parse returns that implements {@link ASTNodeAccess} maps back to its source text. + * Its node spans at least one character, inside the range of the object that holds it. No two + * ranges in a statement partly overlap, and the text of an expression's range parses to that + * expression again. The model is walked through its getters, over the SQL files of the test + * resources. + */ +public class ASTNodeCoverageTest { + private static final Path CORPUS = Paths.get("src/test/resources"); + + @Test + public void testEveryParsedNodeMapsBackToItsSource() throws IOException { + Map problems = new TreeMap<>(); + for (Path file : sqlFiles()) { + String sql = Files.readString(file); + Object statements; + try { + // a statement the parser rejects leaves the rest of its file to check, and the + // Oracle tests parse in the dialect they are written in + boolean oracle = file.toString().contains("oracle-tests"); + statements = CCJSqlParserUtil.parseStatements(sql, parser -> { + parser.withErrorRecovery(); + if (oracle) { + parser.withDialect(AbstractJSqlParser.Dialect.ORACLE); + } + }); + } catch (JSQLParserException e) { + // the corpus also holds SQL the parser rejects + continue; + } + new Walk(file.toString(), sql, problems).check(statements); + } + assertThat(problems).isEmpty(); + } + + @Test + public void testRangesOfStatementsClausesAndTypes() throws JSQLParserException { + String sql = "WITH t AS (SELECT 1) SELECT * FROM t;"; + assertSpans("WITH t AS (SELECT 1) SELECT * FROM t", sql, (ASTNodeAccess) statement(sql)); + + sql = "UPDATE t SET a = 1 WHERE b = 2"; + assertSpans(sql, sql, (ASTNodeAccess) statement(sql)); + + sql = "SELECT /*+ INDEX(t i) */ a FROM t"; + assertSpans("/*+ INDEX(t i) */", sql, ((PlainSelect) statement(sql)).getOracleHint()); + + sql = "SELECT CASE WHEN a = 1 THEN 'x' END, CAST(a AS DECIMAL(10, 2)), s.t.c," + + " ROW_NUMBER() OVER (PARTITION BY a ORDER BY b), SUBSTRING(a FROM 2 FOR 3)" + + " FROM t ORDER BY a DESC NULLS LAST"; + PlainSelect select = (PlainSelect) statement(sql); + assertSpans("WHEN a = 1 THEN 'x'", sql, + ((CaseExpression) item(select, 0)).getWhenClauses().get(0)); + assertSpans("DECIMAL(10, 2)", sql, ((CastExpression) item(select, 1)).getColDataType()); + assertSpans("s.t", sql, ((Column) item(select, 2)).getTable()); + assertSpans("PARTITION BY a", sql, + (ASTNodeAccess) ((AnalyticExpression) item(select, 3)) + .getPartitionExpressionList()); + // the list prints the parentheses of its function + assertSpans("(a FROM 2 FOR 3)", sql, ((Function) item(select, 4)).getNamedParameters()); + assertSpans("a DESC NULLS LAST", sql, select.getOrderByElements().get(0)); + + sql = "SELECT * FROM a, b WHERE a.id(+) = b.id"; + assertSpans("a.id(+)", sql, + ((EqualsTo) ((PlainSelect) statement(sql)).getWhere()).getLeftExpression()); + + sql = "SELECT id FROM t LIMIT 5 BY id"; + assertSpans("LIMIT 5 BY id", sql, ((PlainSelect) statement(sql)).getLimitBy()); + + sql = "CREATE SEQUENCE s INCREMENT BY 1 OWNED BY t.c"; + assertSpans("s INCREMENT BY 1 OWNED BY t.c", sql, + ((CreateSequence) statement(sql)).getSequence()); + + sql = "SELECT 'a'\n'b'"; + Statement merged = CCJSqlParserUtil.parse(sql, parser -> parser.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.NEWLINE)); + assertSpans("'a'\n'b'", sql, item((PlainSelect) merged, 0)); + } + + @Test + public void testStatementsTheCorpusLacks() throws JSQLParserException { + Map problems = new TreeMap<>(); + for (String sql : new String[] { + "WITH x AS (INSERT INTO t VALUES (1) RETURNING *) SELECT * FROM x", + "WITH x AS (UPDATE t SET a = 1 RETURNING *) SELECT * FROM x", + "WITH x AS (DELETE FROM t RETURNING *) SELECT * FROM x", + "CREATE SCHEMA s CREATE TABLE t (a INT) CREATE VIEW v AS SELECT 1", + "CREATE TABLE t AS EXECUTE p", + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO r", + "IF a = 1 SELECT 1;", + "BEGIN SELECT 1; END;"}) { + new Walk(sql, sql, problems).check(CCJSqlParserUtil.parseStatements(sql)); + } + String block = "BEGIN x := 1; NULL; foo(1); END;"; + new Walk(block, block, problems).check(CCJSqlParserUtil.parseStatements(block, + parser -> parser.withDialect(AbstractJSqlParser.Dialect.ORACLE))); + String opaque = "FOO BAR BAZ"; + new Walk(opaque, opaque, problems).check(CCJSqlParserUtil.parseStatements(opaque, + parser -> parser.withUnsupportedStatements(true))); + assertThat(problems).isEmpty(); + } + + @Test + public void testCommentBeforeEndOfInputHasPosition() throws ParseException { + String sql = "SELECT a FROM t -- trailing"; + Statements statements = CCJSqlParserUtil.newParser(sql).Statements(); + Token endOfInput = ((ASTNodeAccess) statements.get(0)).getASTNode().jjtGetLastToken().next; + Token comment = endOfInput.specialToken; + assertEquals("-- trailing", + sql.substring(comment.absoluteBegin - 1, comment.absoluteEnd - 1)); + } + + private static Statement statement(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql); + } + + private static Expression item(PlainSelect select, int index) { + return select.getSelectItem(index).getExpression(); + } + + private static void assertSpans(String expected, String sql, ASTNodeAccess part) { + Node node = part.getASTNode(); + assertEquals(expected, sql.substring(node.jjtGetFirstToken().absoluteBegin - 1, + node.jjtGetLastToken().absoluteEnd - 1), part.getClass().getSimpleName()); + } + + private static List sqlFiles() throws IOException { + try (Stream files = Files.walk(CORPUS)) { + return files.filter(file -> file.toString().endsWith(".sql")).sorted() + .collect(Collectors.toList()); + } + } + + private static final class Walk { + private final String source; + private final String sql; + private final Map problems; + private final Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + private final List ranges = new ArrayList<>(); + + Walk(String source, String sql, Map problems) { + this.source = source; + this.sql = sql; + this.problems = problems; + } + + void check(Object statements) { + visit(statements, null); + checkNoPartialOverlap(); + } + + private void visit(Object value, int[] enclosing) { + if (value == null || !visited.add(value)) { + return; + } + int[] range = range(value); + if (range != null) { + checkRange(value, range, enclosing); + } + int[] inner = range != null ? range : enclosing; + visitElements(value, inner); + if (isModel(value)) { + for (Method getter : value.getClass().getMethods()) { + if (isChildGetter(getter)) { + visitChild(value, getter, inner); + } + } + } + } + + private void visitElements(Object value, int[] enclosing) { + if (value instanceof Collection) { + for (Object element : (Collection) value) { + visit(element, enclosing); + } + } else if (value instanceof Map) { + for (Map.Entry entry : ((Map) value).entrySet()) { + visit(entry.getKey(), enclosing); + visit(entry.getValue(), enclosing); + } + } else if (value instanceof Map.Entry) { + visit(((Map.Entry) value).getKey(), enclosing); + visit(((Map.Entry) value).getValue(), enclosing); + } else if (value instanceof Optional) { + ((Optional) value).ifPresent(present -> visit(present, enclosing)); + } + } + + private void visitChild(Object value, Method getter, int[] enclosing) { + try { + Object child = getter.invoke(value); + if (isHeld(child, getter.invoke(value))) { + visit(child, enclosing); + } + } catch (ReflectiveOperationException | RuntimeException e) { + // a getter that cannot answer for this object has no child to walk + } + } + + /** + * The object's range, or null when it has none, reporting an object that should have one. + */ + private int[] range(Object value) { + // an object that prints as nothing, such as a clause left at its default, has no text + if (!(value instanceof ASTNodeAccess) || value.toString().isBlank()) { + return null; + } + Node node = ((ASTNodeAccess) value).getASTNode(); + if (node == null) { + report("has no node", value); + return null; + } + if (node.parser != null) { + report("keeps its parser alive", value); + } + int begin = node.jjtGetFirstToken().absoluteBegin - 1; + int end = node.jjtGetLastToken().absoluteEnd - 1; + if (begin < 0 || end <= begin || end > sql.length()) { + report("spans no text", value); + return null; + } + return new int[] {begin, end}; + } + + private void checkRange(Object value, int[] range, int[] enclosing) { + if (enclosing != null && (range[0] < enclosing[0] || range[1] > enclosing[1])) { + report("lies outside the object that holds it", value); + } + checkInsideParent(value, range); + ranges.add(new Object[] {range, value}); + String text = sql.substring(range[0], range[1]); + // PRIOR is an operator only inside CONNECT BY + if (value instanceof Expression && !(value instanceof ConnectByPriorOperator)) { + checkReparses(value, text, + expression -> CCJSqlParserUtil.parseExpression(expression, false)); + } else if (value instanceof Statement) { + checkReparses(value, text, CCJSqlParserUtil::parse); + } + } + + private void checkInsideParent(Object value, int[] range) { + ASTNodeAccess parent = value instanceof ASTNodeAccessImpl + ? ((ASTNodeAccessImpl) value).getParent() + : null; + Node parentNode = parent == null ? null : parent.getASTNode(); + if (parentNode != null && (parentNode.jjtGetFirstToken().absoluteBegin - 1 > range[0] + || parentNode.jjtGetLastToken().absoluteEnd - 1 < range[1])) { + report("lies outside its parent " + parent.getClass().getSimpleName(), value); + } + } + + private void checkReparses(Object value, String text, Parse parse) { + Object again; + try { + again = parse.apply(text); + } catch (JSQLParserException e) { + // text that needs its surroundings to parse proves nothing + return; + } + if (again != null + && !normalized(again.toString()).equals(normalized(value.toString()))) { + report("reparses to something else", value, value + " reparses to " + again); + } + } + + // sorted by start, the longer first, ranges nest when each ends inside the one still open + private void checkNoPartialOverlap() { + ranges.sort(Comparator.comparingInt(entry -> ((int[]) entry[0])[0]) + .thenComparingInt(entry -> -((int[]) entry[0])[1])); + Deque open = new ArrayDeque<>(); + for (Object[] entry : ranges) { + int[] range = (int[]) entry[0]; + while (!open.isEmpty() && ((int[]) open.peek()[0])[1] <= range[0]) { + open.pop(); + } + if (!open.isEmpty() && ((int[]) open.peek()[0])[1] < range[1]) { + report("partly overlaps " + open.peek()[1].getClass().getSimpleName(), + entry[1], entry[1]); + } + open.push(entry); + } + } + + private void report(String problem, Object value) { + report(problem, value, value); + } + + private void report(String problem, Object value, Object detail) { + problems.putIfAbsent(value.getClass().getSimpleName() + " " + problem, + source + ": " + detail); + } + } + + private interface Parse { + Object apply(String text) throws JSQLParserException; + } + + private static String normalized(String text) { + return text.replaceAll("\\s+", "").toUpperCase(Locale.ROOT); + } + + // a getter that builds new objects on each call computes them rather than holding them + @SuppressWarnings("PMD.CompareObjectsWithEquals") + private static boolean isHeld(Object child, Object again) { + if (child instanceof ASTNodeAccess) { + return child == again; + } + if (child instanceof Collection && !((Collection) child).isEmpty()) { + return again instanceof Collection && !((Collection) again).isEmpty() + && ((Collection) child).iterator().next() == ((Collection) again) + .iterator().next(); + } + return true; + } + + private static boolean isModel(Object value) { + String name = value.getClass().getName(); + return name.startsWith("net.sf.jsqlparser.") + && !name.startsWith("net.sf.jsqlparser.parser.") + && !(value instanceof Enum); + } + + private static boolean isChildGetter(Method method) { + return method.getParameterCount() == 0 && method.getName().startsWith("get") + && !Modifier.isStatic(method.getModifiers()) + && !method.getName().equals("getParent") && !method.getName().equals("getASTNode"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserTest.java index f0093e11f8..d408fc68fb 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserTest.java @@ -10,10 +10,14 @@ package net.sf.jsqlparser.parser; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import net.sf.jsqlparser.JSQLParserException; import org.junit.jupiter.api.Test; import net.sf.jsqlparser.parser.feature.Feature; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; public class CCJSqlParserTest { @Test @@ -24,4 +28,21 @@ public void parserWithTimeout() throws Exception { assertThat(timeOut).isEqualTo(123L); } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "SELECT /* c */1 | SELECT /* c */ 1", + "SELECT 1 /* c */+2 | SELECT 1 /* c */ +2", + "SELECT 1/*x*/+2 | SELECT 1 /*x*/ +2", + "SELECT /* a /* b */ c */1 | SELECT /* a /* b */ c */ 1", + "SELECT * FROM t WHERE/*c*/x=1 | SELECT * FROM t WHERE /*c*/ x=1", + "SELECT/*+ FULL(t) */*FROM t | SELECT /*+ FULL(t) */ * FROM t" + }) + void testBlockCommentFollowedByNonWhitespace(String tight, String spaced) + throws JSQLParserException { + // Issue #2509: the first character after */ was swallowed into the comment token, + // so the tight form either failed to parse or silently lost that character. + assertEquals(CCJSqlParserUtil.parse(spaced).toString(), + CCJSqlParserUtil.parse(tight).toString()); + } } diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index dd9b557325..bf74eb6a3c 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -12,14 +12,18 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication; import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.Statements; import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.create.table.ColDataType; import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.TableStatement; import net.sf.jsqlparser.test.MemoryLeakVerifier; import net.sf.jsqlparser.test.TestUtils; @@ -37,6 +41,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,6 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class CCJSqlParserUtilTest { @@ -104,6 +111,34 @@ public void testParseExpression2() throws Exception { assertInstanceOf(ParenthesedExpressionList.class, mult.getRightExpression()); } + @Test + public void testParseColDataType() throws Exception { + assertNull(CCJSqlParserUtil.parseColDataType(null)); + assertNull(CCJSqlParserUtil.parseColDataType("")); + + ColDataType arrayType = CCJSqlParserUtil.parseColDataType("integer[][]"); + assertEquals("integer", arrayType.getDataType()); + assertEquals(2, arrayType.getArrayData().size()); + assertNull(arrayType.getArrayData().get(0)); + assertNull(arrayType.getArrayData().get(1)); + + AtomicBoolean configured = new AtomicBoolean(); + ColDataType enumType = + CCJSqlParserUtil.parseColDataType("enum('small','medium')", parser -> { + configured.set(true); + parser.withDialect(AbstractJSqlParser.Dialect.MYSQL); + }); + assertTrue(configured.get()); + assertEquals("enum", enumType.getDataType()); + assertEquals(List.of("'small'", "'medium'"), enumType.getArgumentsStringList()); + } + + @Test + public void testParseColDataTypeRejectsTrailingTokens() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseColDataType("varchar(255) NOT NULL")); + } + @Test public void testParseExpressionNonPartial() throws Exception { assertThrows(JSQLParserException.class, @@ -487,9 +522,39 @@ void testUnbalancedPosition() { } @Test - void testParseEmpty() throws JSQLParserException { - assertNull(CCJSqlParserUtil.parse("")); - assertNull(CCJSqlParserUtil.parse((String) null)); + void testParseEmpty() { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse("")); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse((String) null)); + } + + @Test + void testParseRejectsUnconsumedInput() throws Exception { + // "\n\n\n" separates statements exactly like ";" does, so both inputs below hold + // two statements; a single statement parse must not silently return the first only + String newlineSeparated = "SELECT a FROM dual WHERE x = ?\n\n\n AND y = ?"; + String semicolonSeparated = "SELECT a FROM dual; SELECT b FROM dual"; + + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(newlineSeparated)); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(semicolonSeparated)); + assertThrows(ParseException.class, + () -> CCJSqlParserUtil.newParser(newlineSeparated).Statement()); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse(new ByteArrayInputStream( + newlineSeparated.getBytes(StandardCharsets.UTF_8)))); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parseAST(newlineSeparated)); + + // trailing separators and comments do not make a statement incomplete + assertEquals("SELECT a FROM dual", + CCJSqlParserUtil.parse("SELECT a FROM dual;").toString()); + assertEquals("SELECT a FROM dual", + CCJSqlParserUtil.parse("SELECT a FROM dual\n\n\n").toString()); + assertEquals("SELECT a FROM dual", + CCJSqlParserUtil.parse("SELECT a FROM dual -- done").toString()); + + // the multi statement API keeps working + assertEquals(2, CCJSqlParserUtil.parseStatements(semicolonSeparated).size()); + assertEquals(3, CCJSqlParserUtil.parseStatements( + "SELECT * FROM DUAL\n\n\nSELECT * FROM DUAL\n\n\n\nSELECT * FROM dual").size()); } @Test @@ -514,4 +579,445 @@ void testSingleStatementWithEmptyLines() throws JSQLParserException { + "def'\n" + "where id=?", true); } + + @Test + void testBlankLinesAsSeparatorDisabled() throws JSQLParserException { + Consumer blankLinesAsWhitespace = p -> p.withBlankLinesAsSeparator(false); + + assertEquals("SELECT a FROM dual WHERE x = ? AND y = ?", + CCJSqlParserUtil.parse("SELECT a FROM dual WHERE x = ?\n\n\n AND y = ?", + blankLinesAsWhitespace).toString()); + assertEquals("SELECT a, b FROM t", + CCJSqlParserUtil.parse("SELECT a,\n\n\n\nb FROM t", blankLinesAsWhitespace) + .toString()); + assertEquals("SELECT a FROM t WHERE a > 1", + CCJSqlParserUtil.parse("SELECT a FROM t\n\n\nWHERE a > 1", blankLinesAsWhitespace) + .toString()); + assertEquals("SELECT a FROM t UNION SELECT a FROM u", + CCJSqlParserUtil.parse("SELECT a FROM t\n\n\n\n\nUNION\n\n\nSELECT a FROM u", + blankLinesAsWhitespace).toString()); + assertEquals("SELECT a FROM t JOIN u ON t.id = u.id", + CCJSqlParserUtil.parse("SELECT a FROM t\n\n\nJOIN u ON t.id = u.id", + blankLinesAsWhitespace).toString()); + assertEquals("SELECT a FROM dual", + CCJSqlParserUtil.parse("SELECT a FROM dual\n\n\n", blankLinesAsWhitespace) + .toString()); + assertEquals("SELECT 'a\n\n\nb' FROM dual", + CCJSqlParserUtil.parse("SELECT 'a\n\n\nb' FROM dual", blankLinesAsWhitespace) + .toString()); + + assertEquals(1, CCJSqlParserUtil.parseStatements( + "SELECT a FROM t\n\n\nWHERE a > 1", blankLinesAsWhitespace).size()); + assertEquals(3, CCJSqlParserUtil.parseStatements( + "SELECT 1 FROM dual;\n\n\nSELECT 2 FROM dual\ngo\nSELECT 3 FROM dual\n/\n", + blankLinesAsWhitespace).size()); + + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT a\n\n\n\nFROM", blankLinesAsWhitespace)); + assertTrue(exception.getCause().getMessage().contains("at line 5, column 1"), + exception.getCause().getMessage()); + } + + @Test + public void testHashLineCommentsFeature() throws Exception { + // with the flag: MySQL line comments, `#` to end of line, + // unconditional like MySQL itself (no blank needed, `42#24` is a + // comment there too); the statement continues on the next line + assertEquals("SELECT 42", CCJSqlParserUtil + .parse("SELECT 42 # 24", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 # comment, 2", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 #comment", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 #!bang", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 42", CCJSqlParserUtil + .parse("SELECT 42#24", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT a", CCJSqlParserUtil + .parse("SELECT a#b FROM t", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1 FROM t", CCJSqlParserUtil + .parse("SELECT 1 # c\nFROM t", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("# leading\nSELECT 1", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 #", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 #\n", p -> p.withHashLineComments(true)).toString()); + // the `#>` family has no meaning in MySQL and comments out like any + // other `#`; quoted forms keep their `#` + assertEquals("SELECT data", CCJSqlParserUtil + .parse("SELECT data #> '{a}' FROM t", p -> p.withHashLineComments(true)) + .toString()); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT '#' FROM t", true, + p -> p.withHashLineComments(true)); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT \"a#b\" FROM t", true, + p -> p.withHashLineComments(true)); + // default (flag off): the #2507 binary operator, unchanged + assertEquals("SELECT 42 # 24", CCJSqlParserUtil.parse("SELECT 42 # 24").toString()); + assertEquals("SELECT 42#24", CCJSqlParserUtil.parse("SELECT 42#24").toString()); + } + + @Test + public void testHashLineCommentsMySQLStatementSemantics() throws Exception { + // `#temp` is a comment under the flag, so `SELECT #temp FROM t` + // loses the rest of the line and fails, exactly like MySQL; with the + // flag off the same input parses as the #2507 operator expression + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("SELECT #temp FROM t", p -> p.withHashLineComments(true))); + assertEquals("SELECT 1 # comment", + CCJSqlParserUtil.parse("SELECT 1 # comment").toString()); + } + + @Test + public void testDialectPresets() throws Exception { + // MYSQL and MARIADB: backslash escapes and # line comments (MySQL 8 + // manual "Comments" and "String Literals", MariaDB KB "Comment Syntax" + // and "String Literals"); backticks are always on, brackets are not + // MySQL syntax + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.MYSQL, AbstractJSqlParser.Dialect.MARIADB}) { + assertEquals("SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42", CCJSqlParserUtil + .parse("SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42#24", + p -> p.withDialect(dialect)) + .toString()); + } + // SQLSERVER: bracket quotation only (Microsoft Learn "Database + // Identifiers"), `#` stays available for identifiers (temp tables) + assertEquals("SELECT [my column] FROM t", + CCJSqlParserUtil.parse("SELECT [my column] FROM t", + p -> p.withDialect(AbstractJSqlParser.Dialect.SQLSERVER)).toString()); + // empty presets keep the defaults: the #2507 operator, no backslash + // escapes + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.ANSI_SQL, AbstractJSqlParser.Dialect.ORACLE, + AbstractJSqlParser.Dialect.POSTGRESQL, AbstractJSqlParser.Dialect.H2, + AbstractJSqlParser.Dialect.EXASOL}) { + assertEquals("SELECT 42 # 24", + CCJSqlParserUtil.parse("SELECT 42 # 24", p -> p.withDialect(dialect)) + .toString()); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("SELECT 'a\\'b'", p -> p.withDialect(dialect))); + } + } + + @Test + public void testDialectPresetSwitchOverrides() throws Exception { + // an explicit switch after the preset wins: `#` is the #2507 + // operator again + assertEquals("SELECT 1 # comment", CCJSqlParserUtil.parse("SELECT 1 # comment", + p -> p.withDialect(AbstractJSqlParser.Dialect.MYSQL).withHashLineComments(false)) + .toString()); + // the preset never turns a previously enabled switch off + assertEquals("SELECT [my column] FROM mytable", + CCJSqlParserUtil.parse("SELECT [my column] FROM mytable", + p -> p.withSquareBracketQuotation(true) + .withDialect(AbstractJSqlParser.Dialect.MYSQL)) + .toString()); + } + + @Test + public void testDoubleQuotedStringsFeature() throws Exception { + // default (flag off): double quotes are quoted identifiers, unchanged + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil + .parse("SELECT \"not an identifier\"")).getSelectBody(); + assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column); + // on: the BigQuery/Spark/MySQL-default reading, double quotes are + // string literals, the quote kept on round-trip + select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT \"not an identifier\"", + p -> p.withDoubleQuotedStrings(true))).getSelectBody(); + Expression expression = select.getSelectItems().get(0).getExpression(); + assertTrue(expression instanceof StringValue); + assertEquals("not an identifier", ((StringValue) expression).getValue()); + assertEquals("SELECT \"not an identifier\"", select.toString()); + // empty string and doubled quotes take the single-quote treatment + assertEquals("SELECT \"\"", CCJSqlParserUtil + .parse("SELECT \"\"", p -> p.withDoubleQuotedStrings(true)).toString()); + assertEquals("SELECT \"a\"\"b\"", CCJSqlParserUtil + .parse("SELECT \"a\"\"b\"", p -> p.withDoubleQuotedStrings(true)).toString()); + // identifier positions: a `"..."` token follows the existing + // string-as-table branch (`FROM 'file.csv'`), the same leniency + // single quotes already have; MySQL itself would error here + assertEquals("SELECT * FROM \"t\"", CCJSqlParserUtil + .parse("SELECT * FROM \"t\"", p -> p.withDoubleQuotedStrings(true)).toString()); + } + + @Test + public void testDialectPresetsWarehouses() throws Exception { + // BIGQUERY: double quoted strings, # line comments and backslash + // escapes (GoogleSQL "Lexical structure and syntax", identifiers are + // backticked), backticks stay unconditional + assertEquals("SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42", CCJSqlParserUtil + .parse("SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42#24", + p -> p.withDialect(AbstractJSqlParser.Dialect.BIGQUERY)) + .toString()); + // DATABRICKS: double quoted strings and backslash escapes (Spark + // "Literals"), `#` stays the #2507 operator + assertEquals("SELECT \"a\" FROM t WHERE x = 'it\\'s'", CCJSqlParserUtil + .parse("SELECT \"a\" FROM t WHERE x = 'it\\'s'", + p -> p.withDialect(AbstractJSqlParser.Dialect.DATABRICKS)) + .toString()); + assertEquals("SELECT 42 # 24", CCJSqlParserUtil.parse("SELECT 42 # 24", + p -> p.withDialect(AbstractJSqlParser.Dialect.DATABRICKS)).toString()); + // SNOWFLAKE: backslash escape sequences ("String & binary data + // types"), `#` stays the operator + assertEquals("SELECT col FROM t WHERE a = 'x\\'yz'", CCJSqlParserUtil + .parse("SELECT col FROM t WHERE a = 'x\\'yz'", + p -> p.withDialect(AbstractJSqlParser.Dialect.SNOWFLAKE)) + .toString()); + assertEquals("SELECT 42 # 24", CCJSqlParserUtil.parse("SELECT 42 # 24", + p -> p.withDialect(AbstractJSqlParser.Dialect.SNOWFLAKE)).toString()); + } + + @Test + public void testDoubleQuotedStringsPreset() throws Exception { + // the string-default dialects: MYSQL and MARIADB (default sql_mode), + // BIGQUERY and DATABRICKS (string literals, identifiers are backticked) + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.MYSQL, AbstractJSqlParser.Dialect.MARIADB, + AbstractJSqlParser.Dialect.BIGQUERY, AbstractJSqlParser.Dialect.DATABRICKS}) { + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil + .parse("SELECT \"abc\"", p -> p.withDialect(dialect))).getSelectBody(); + assertTrue(select.getSelectItems().get(0).getExpression() instanceof StringValue); + } + // the identifier-default dialects keep the quoted identifier reading + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.SQLSERVER, AbstractJSqlParser.Dialect.SNOWFLAKE}) { + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT \"abc\"", + p -> p.withDialect(dialect))).getSelectBody(); + assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column); + } + } + + @Test + public void testDoubleQuotedStringsWithBackslashEscapes() throws Exception { + // MySQL reads "it\"s" as it"s; the escaped quote must not end the literal + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.MYSQL, AbstractJSqlParser.Dialect.MARIADB, + AbstractJSqlParser.Dialect.BIGQUERY, AbstractJSqlParser.Dialect.DATABRICKS}) { + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse( + "SELECT \"it\\\"s\", \"a\\\\\"", p -> p.withDialect(dialect))) + .getSelectBody(); + assertEquals("it\\\"s", + ((StringValue) select.getSelectItems().get(0).getExpression()).getValue()); + assertEquals("a\\\\", + ((StringValue) select.getSelectItems().get(1).getExpression()).getValue()); + assertEquals("SELECT \"it\\\"s\", \"a\\\\\"", select.toString()); + } + // a doubled quote still continues a MySQL literal + assertEquals("SELECT \"b\"\"c\\\"d\"", CCJSqlParserUtil.parse("SELECT \"b\"\"c\\\"d\"", + p -> p.withDialect(AbstractJSqlParser.Dialect.MYSQL)).toString()); + String alter = "ALTER TABLE t ADD COLUMN s VARCHAR (10) DEFAULT \"it\\\"s\""; + assertEquals(alter, CCJSqlParserUtil + .parse(alter, p -> p.withDialect(AbstractJSqlParser.Dialect.MYSQL)).toString()); + // the same escape needs both features + assertEquals("SELECT \"it\\\"s\"", CCJSqlParserUtil.parse("SELECT \"it\\\"s\"", + p -> p.withDoubleQuotedStrings(true).withBackslashEscapeCharacter(true)) + .toString()); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("SELECT \"it\\\"s\"", p -> p.withDoubleQuotedStrings(true))); + // MySQL closes "a\"" before b, so the rest is unterminated as on the server + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "SELECT \"a\\\"\"b\"", p -> p.withDialect(AbstractJSqlParser.Dialect.MYSQL))); + // quoted identifiers keep a trailing backslash + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] {null, + AbstractJSqlParser.Dialect.POSTGRESQL, AbstractJSqlParser.Dialect.SNOWFLAKE}) { + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse( + "SELECT \"col\\\" FROM \"t\"", p -> { + if (dialect != null) { + p.withDialect(dialect); + } + })).getSelectBody(); + assertEquals("\"col\\\"", + ((Column) select.getSelectItems().get(0).getExpression()).getColumnName()); + assertEquals("SELECT \"col\\\" FROM \"t\"", select.toString()); + } + } + + @Test + public void testSingleQuotedStringsMixBackslashEscapesAndDoubledQuotes() throws Exception { + // each is one MySQL string; the first used to split into a string and an alias + for (String literal : new String[] {"'it\\'s ''x'''", "'a\\0b\\'c'", "'\\Z\\''", + "'a''b\\'c'", "'x\\%\\'y'", "N'it\\'s'"}) { + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse( + "SELECT " + literal + " FROM t", + p -> p.withDialect(AbstractJSqlParser.Dialect.MYSQL))).getSelectBody(); + assertEquals(1, select.getSelectItems().size(), literal); + assertNull(select.getSelectItems().get(0).getAlias(), literal); + assertInstanceOf(StringValue.class, select.getSelectItems().get(0).getExpression()); + assertEquals("SELECT " + literal + " FROM t", select.toString()); + } + // PostgreSQL E'' strings escape quotes; standard strings keep a literal backslash + assertEquals("SELECT E'it\\'s ''x'''", CCJSqlParserUtil.parse("SELECT E'it\\'s ''x'''", + p -> p.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL)).toString()); + assertEquals("SELECT 'a\\', 'b'", CCJSqlParserUtil.parse("SELECT 'a\\', 'b'", + p -> p.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL)).toString()); + // without backslash escapes the literal still ends at the escaped-looking quote + assertEquals("SELECT 'a\\' AS x", CCJSqlParserUtil.parse("SELECT 'a\\' AS x").toString()); + assertEquals("SELECT * FROM t WHERE a LIKE 'x\\_%' ESCAPE '\\'", CCJSqlParserUtil + .parse("SELECT * FROM t WHERE a LIKE 'x\\_%' ESCAPE '\\'").toString()); + } + + @Test + public void testPostgresUnicodeEscapeStringsAreOneLiteral() throws Exception { + Consumer postgres = + p -> p.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL); + for (String literal : new String[] {"U&'\\0041BC \\00e9'", + "U&'d!0061t!+000061' UESCAPE '!'", "U&'it''s'", "U&'a\\\\b'", + "U&'\\' UESCAPE '!'", "U&'x' UESCAPE '\\'"}) { + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse( + "SELECT " + literal + " FROM t", postgres)).getSelectBody(); + assertEquals(1, select.getSelectItems().size(), literal); + assertNull(select.getSelectItems().get(0).getAlias(), literal); + assertInstanceOf(StringValue.class, select.getSelectItems().get(0).getExpression()); + assertEquals("SELECT " + literal + " FROM t", select.toString()); + } + // the prefix is case-insensitive; UESCAPE may follow without or after any whitespace + assertEquals("SELECT U&'#0041' UESCAPE '#', U&'x' FROM t", CCJSqlParserUtil.parse( + "SELECT u&'#0041'\n\tuescape\n'#', U&'x'FROM t", postgres).toString()); + // spaces inside U&' make it an expression + assertInstanceOf(BitwiseAnd.class, ((PlainSelect) CCJSqlParserUtil + .parse("SELECT U & 'x' FROM t", postgres)).getSelectItem(0).getExpression()); + assertInstanceOf(BitwiseAnd.class, ((PlainSelect) CCJSqlParserUtil + .parse("SELECT U &'x' FROM t", postgres)).getSelectItem(0).getExpression()); + } + + @Test + public void testUnicodeEscapeStringsNeedThePostgresDialect() throws Exception { + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] {null, + AbstractJSqlParser.Dialect.MYSQL, AbstractJSqlParser.Dialect.ORACLE, + AbstractJSqlParser.Dialect.SQLSERVER, AbstractJSqlParser.Dialect.ANSI_SQL}) { + Consumer consumer = p -> { + if (dialect != null) { + p.withDialect(dialect); + } + }; + for (String sql : new String[] {"SELECT U&'x' FROM t", + "SELECT u&'\\0041' FROM t"}) { + BitwiseAnd and = assertInstanceOf(BitwiseAnd.class, ((PlainSelect) CCJSqlParserUtil + .parse(sql, consumer)).getSelectItem(0).getExpression(), sql); + assertInstanceOf(Column.class, and.getLeftExpression(), sql); + assertNull(assertInstanceOf(StringValue.class, and.getRightExpression(), sql) + .getPrefix(), sql); + } + for (String sql : new String[] {"SELECT a & b FROM t", "SELECT U & 1 FROM t", + "SELECT U&1 FROM t", "SELECT U & 'x' FROM t"}) { + assertInstanceOf(BitwiseAnd.class, ((PlainSelect) CCJSqlParserUtil.parse(sql, + consumer)).getSelectItem(0).getExpression(), sql); + } + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("SELECT U&'d!0061' UESCAPE '!' FROM t", consumer)); + // literals that merely start with & are unaffected + assertEquals("SELECT '&', '&x', N'&', 'a' & 'b' FROM t", CCJSqlParserUtil + .parse("SELECT '&', '&x', N'&', 'a' & 'b' FROM t", consumer).toString()); + } + // the bitwise reading also stays for PostgreSQL's U & 1 and a & b + for (String sql : new String[] {"SELECT a & b FROM t", "SELECT U & 1 FROM t", + "SELECT U&1 FROM t", "SELECT '&' & 'b' FROM t"}) { + assertInstanceOf(BitwiseAnd.class, ((PlainSelect) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL))) + .getSelectItem(0).getExpression(), sql); + } + } + + @Test + public void testInvalidUnicodeEscapeStringsAreRejected() { + for (String literal : new String[] {"U&'\\00'", "U&'\\0000'", "U&'\\+110000'", + "U&'\\+12'", "U&'\\D800'", "U&'\\DC00'", "U&'\\D800x'", + "U&'\\D800\\0041'", "U&'\\D800\\\\'", "U&'x\\'", "U&'\\g000'", + "U&'x' UESCAPE '+'", "U&'x' UESCAPE 'a'", "U&'x' UESCAPE 'F'", + "U&'x' UESCAPE ' '", "U&'x' UESCAPE '\"'", "U&'x' UESCAPE '\u00e9'"}) { + for (String sql : new String[] {"SELECT " + literal, "SELECT f(" + literal + ")", + "COMMENT ON TABLE t IS " + literal}) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + p -> p.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL)), sql); + } + } + } + + @Test + public void testAdjacentStringLiteralsNewline() throws Exception { + // default (off): the MySQL / SQL Server alias reading, unchanged + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a' 'b'").toString()); + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'").toString()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE x = 'a'\n'b'")); + // newline mode: the standard / Postgres reading, "Two string constants + // that are only separated by whitespace with at least one newline are + // concatenated" + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.NEWLINE))) + .getSelectBody(); + Expression expression = select.getSelectItems().get(0).getExpression(); + assertTrue(expression instanceof StringValue); + assertEquals("ab", ((StringValue) expression).getValue()); + assertEquals("SELECT 'ab'", select.toString()); + // same line keeps the alias reading under newline mode + assertEquals("SELECT 'a' 'b'", + CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.NEWLINE)) + .toString()); + // expression positions concatenate too + assertEquals("SELECT * FROM t WHERE x = 'ab'", + CCJSqlParserUtil.parse("SELECT * FROM t WHERE x = 'a'\n'b'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.NEWLINE)) + .toString()); + // three parts merge into one literal + assertEquals("SELECT 'abc'", + CCJSqlParserUtil.parse("SELECT 'a'\n'b'\n'c'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.NEWLINE)) + .toString()); + // the ANSI SQL and Postgres presets carry it, the MySQL preset does not + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.ANSI_SQL, AbstractJSqlParser.Dialect.POSTGRESQL}) { + assertEquals("SELECT 'ab'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withDialect(dialect)).toString()); + } + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withDialect(AbstractJSqlParser.Dialect.MYSQL)).toString()); + } + + @Test + public void testAdjacentStringLiteralsWhitespace() throws Exception { + // whitespace mode: the GoogleSQL / Spark reading, same line + // concatenates (their literal chunking) + assertEquals("SELECT 'ab'", + CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.WHITESPACE)) + .toString()); + // combined with allowDoubleQuotedStrings: the BigQuery chunking shape + assertEquals("SELECT \"12\"", + CCJSqlParserUtil.parse("SELECT \"1\" \"2\"", + p -> p.withDoubleQuotedStrings(true).withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.WHITESPACE)) + .toString()); + // the BigQuery and Databricks presets carry it, the Snowflake preset + // does not (no adjacent-literal concatenation documented) + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.BIGQUERY, AbstractJSqlParser.Dialect.DATABRICKS}) { + assertEquals("SELECT 'ab'", CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withDialect(dialect)).toString()); + } + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withDialect(AbstractJSqlParser.Dialect.SNOWFLAKE)).toString()); + } + + @Test + public void testAdjacentStringLiteralsBoolean() throws Exception { + // true: the standard (newline) mode, the same line keeps the alias reading + assertEquals("SELECT 'ab'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withAdjacentStringLiterals(true)).toString()); + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withAdjacentStringLiterals(true)).toString()); + // false: off, the alias reading also across newlines + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withAdjacentStringLiterals(false)).toString()); + // the no-arg variant enables the standard mode + assertEquals("SELECT 'ab'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withAdjacentStringLiterals()).toString()); + } } diff --git a/src/test/java/net/sf/jsqlparser/parser/ContextualTokenTest.java b/src/test/java/net/sf/jsqlparser/parser/ContextualTokenTest.java new file mode 100644 index 0000000000..61dc9aa3e3 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/ContextualTokenTest.java @@ -0,0 +1,78 @@ +/*- + * #%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.parser; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.alter.AlterSystemStatement; +import net.sf.jsqlparser.statement.alter.AlterSystemOperation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +class ContextualTokenTest { + @ParameterizedTest + @ValueSource(strings = { + "SELECT a FROM t UNION ALL BY NAME MATCHING (a) SELECT a FROM u", + "SELECT a FROM t UNION DISTINCT BY NAME MATCHING (a, b) SELECT a FROM u", + "ALTER SYSTEM ENABLE DISTRIBUTED RECOVERY", + "ALTER SYSTEM DISABLE DISTRIBUTED RECOVERY" + }) + void parsesPreviouslyShadowedContextualKeywords(String sql) throws Exception { + for (boolean complex : new boolean[] {false, true}) { + String parsed = assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withAllowComplexParsing(complex)).toString(); + assertEquals(parsed, CCJSqlParserUtil.parse(parsed).toString()); + } + } + + @Test + void preservesAlterSystemOperation() throws Exception { + assertEquals(AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY, + ((AlterSystemStatement) CCJSqlParserUtil.parse( + "alter system enable distributed recovery")).getOperation()); + assertEquals(AlterSystemOperation.DISABLE_DISTRIBUTED_RECOVERY, + ((AlterSystemStatement) CCJSqlParserUtil.parse( + "alter system disable distributed recovery")).getOperation()); + } + + @ParameterizedTest + @ValueSource(strings = {"MATCHING", "DISTRIBUTED", "RECOVERY"}) + void keepsContextualWordsUsableAsIdentifiers(String word) throws Exception { + assertEquals(CCJSqlParserConstants.S_IDENTIFIER, + CCJSqlParserUtil.newParser(word).getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("SELECT " + word + ", " + word + + "(a) FROM " + word + " AS x", true); + } + + @ParameterizedTest + @ValueSource( + strings = {"BYTES", "BINARY", "BOOLEAN", "UNSIGNED", "SIGNED", "CHARACTER", "CHAR"}) + void retainsDedicatedTypeTokensAndTypeParsing(String type) throws Exception { + assertNotEquals(CCJSqlParserConstants.DATA_TYPE, + CCJSqlParserUtil.newParser(type).getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (c " + type + ")", true); + assertSqlCanBeParsedAndDeparsed("SELECT CAST(c AS " + type + ") FROM t", true); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER SYSTEM ENABLE other RECOVERY", + "ALTER SYSTEM DISABLE DISTRIBUTED other", + "ALTER SYSTEM ENABLE DISTRIBUTED", + "SELECT a FROM t UNION ALL BY NAME other (a) SELECT a FROM u", + "SELECT a FROM t UNION ALL BY NAME MATCHING () SELECT a FROM u" + }) + void rejectsWrongContextualKeywords(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/DialectPresetIsolationTest.java b/src/test/java/net/sf/jsqlparser/parser/DialectPresetIsolationTest.java new file mode 100644 index 0000000000..e1c1bd56dd --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/DialectPresetIsolationTest.java @@ -0,0 +1,48 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.feature.Feature; +import org.junit.jupiter.api.Test; + +class DialectPresetIsolationTest { + @Test + void callersCannotMutateSharedPresets() { + for (Dialect dialect : Dialect.values()) { + assertThrows(UnsupportedOperationException.class, + () -> dialect.getLexerFeatures().add(Feature.allowHashLineComments)); + assertThrows(UnsupportedOperationException.class, + () -> dialect.getLexerFeatures().clear()); + } + } + + @Test + void explicitParserOverridesAreLocalAndDoNotModifyPresetDefaults() { + CCJSqlParser first = CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.MYSQL) + .withHashLineComments(false); + CCJSqlParser second = CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.MYSQL); + assertFalse(first.getAsBoolean(Feature.allowHashLineComments)); + assertTrue(second.getAsBoolean(Feature.allowHashLineComments)); + assertTrue(Dialect.MYSQL.getLexerFeatures().contains(Feature.allowHashLineComments)); + } + + @Test + void preservesAdditivePresetApplicationAndExplicitOverrideOrder() { + CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.SQLSERVER) + .withDialect(Dialect.POSTGRESQL); + assertTrue(parser.getAsBoolean(Feature.allowSquareBracketQuotation)); + parser.withSquareBracketQuotation(false); + assertFalse(parser.getAsBoolean(Feature.allowSquareBracketQuotation)); + assertFalse(CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.POSTGRESQL) + .getAsBoolean(Feature.allowSquareBracketQuotation)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/EmptyStatementInputTest.java b/src/test/java/net/sf/jsqlparser/parser/EmptyStatementInputTest.java new file mode 100644 index 0000000000..9d7f87eb72 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/EmptyStatementInputTest.java @@ -0,0 +1,89 @@ +/*- + * #%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.parser; + +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.Statements; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class EmptyStatementInputTest { + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" \t\r\n", "/* nothing */", "-- nothing\n"}) + void rejectsInputWithoutASingleStatement(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (boolean allowComplex : new boolean[] {false, true}) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withAllowComplexParsing(allowComplex))); + } + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" \t\r\n", "/* nothing */", "-- nothing\n"}) + void returnsAnEmptyStatementList(String sql) throws Exception { + assertTrue(CCJSqlParserUtil.parseStatements(sql).isEmpty()); + for (boolean allowComplex : new boolean[] {false, true}) { + assertTrue(CCJSqlParserUtil.parseStatements(sql, + parser -> parser.withAllowComplexParsing(allowComplex)).isEmpty()); + } + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" \t\r\n", "/* nothing */", "-- nothing\n"}) + void preservesTheContractWithACallerExecutor(String sql) throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + for (boolean allowComplex : new boolean[] {false, true}) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, executor, + parser -> parser.withAllowComplexParsing(allowComplex))); + assertTrue(CCJSqlParserUtil.parseStatements(sql, executor, + parser -> parser.withAllowComplexParsing(allowComplex)).isEmpty()); + } + assertFalse(executor.isShutdown()); + assertEquals("SELECT 1", CCJSqlParserUtil.parse("SELECT 1", executor, null).toString()); + assertEquals(2, + CCJSqlParserUtil.parseStatements("SELECT 1; SELECT 2", executor, null).size()); + } finally { + executor.shutdownNow(); + } + } + + @ParameterizedTest + @ValueSource(strings = {"", " \t\r\n", "/* nothing */", "-- nothing\n"}) + void agreesWithReaderAndStreamParsing(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(new StringReader(sql))); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + new ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8)))); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + new ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8)), "UTF-8")); + } + + @Test + void emptyResultsRemainIndependentAndMutable() throws Exception { + Statements first = CCJSqlParserUtil.parseStatements(""); + first.add(CCJSqlParserUtil.parse("SELECT 1")); + assertTrue(CCJSqlParserUtil.parseStatements("").isEmpty()); + assertTrue(CCJSqlParserUtil.parseStatements((String) null).isEmpty()); + assertEquals(1, first.size()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/ExpressionParsingFallbackTest.java b/src/test/java/net/sf/jsqlparser/parser/ExpressionParsingFallbackTest.java new file mode 100644 index 0000000000..924e8ef85a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/ExpressionParsingFallbackTest.java @@ -0,0 +1,129 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.feature.Feature; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class ExpressionParsingFallbackTest { + private static final String PARTIAL_EXPRESSION = "[customer].[id] = 1 trailing tokens"; + + @ParameterizedTest + @EnumSource(ParserApi.class) + void configuresCompleteExpressionsOnce(ParserApi api) throws JSQLParserException { + List attempts = new ArrayList<>(); + Expression result = api.parse("[customer].[id] = 1", false, parser -> { + attempts.add(parser); + parser.withSquareBracketQuotation(true); + }); + + assertEquals("[customer].[id] = 1", result.toString()); + assertEquals(1, attempts.size()); + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void reappliesConfigurationToAFreshParserForPartialInput(ParserApi api) + throws JSQLParserException { + List attempts = new ArrayList<>(); + List modes = new ArrayList<>(); + Expression result = api.parse(PARTIAL_EXPRESSION, true, parser -> { + attempts.add(parser); + modes.add(parser.getAsBoolean(Feature.allowComplexParsing)); + parser.withSquareBracketQuotation(true); + }); + + assertEquals("[customer].[id] = 1", result.toString()); + assertEquals(List.of(false, true), modes); + assertNotSame(attempts.get(0), attempts.get(1)); + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void rejectsTrailingTokensWhenPartialParsingIsDisabled(ParserApi api) { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> api.parse(PARTIAL_EXPRESSION, false, + parser -> parser.withSquareBracketQuotation(true))); + + assertEquals("could only parse partial expression [customer].[id] = 1", + exception.getMessage()); + assertNull(exception.getCause()); + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void preservesSyntaxFailureCauses(ParserApi api) { + for (boolean partial : new boolean[] {false, true}) { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> api.parse(";", partial, null)); + + ParseException cause = assertInstanceOf(ParseException.class, exception.getCause()); + assertEquals(cause.getMessage(), exception.getMessage()); + } + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void leavesNullAndEmptyInputUnconfigured(ParserApi api) throws JSQLParserException { + Consumer unexpectedConfiguration = parser -> { + throw new AssertionError("Empty input must not configure a parser"); + }; + for (boolean partial : new boolean[] {false, true}) { + assertNull(api.parse(null, partial, unexpectedConfiguration)); + assertNull(api.parse("", partial, unexpectedConfiguration)); + } + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void acceptsNullConfiguration(ParserApi api) throws JSQLParserException { + assertEquals("x = 92", api.parse("x=92", false, null).toString()); + assertEquals("x = 92", api.parse("x=92 trailing tokens", true, null).toString()); + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void propagatesConfigurationFailuresWithoutRetrying(ParserApi api) { + IllegalStateException failure = new IllegalStateException("Configuration failed"); + List attempts = new ArrayList<>(); + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> api.parse("x=92", false, parser -> { + attempts.add(parser); + throw failure; + })); + + assertSame(failure, exception); + assertEquals(1, attempts.size()); + } + + private enum ParserApi { + EXPRESSION, CONDITION; + + Expression parse(String text, boolean allowPartialParse, Consumer consumer) + throws JSQLParserException { + return this == EXPRESSION + ? CCJSqlParserUtil.parseExpression(text, allowPartialParse, consumer) + : CCJSqlParserUtil.parseCondExpression(text, allowPartialParse, consumer); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/LargeObjectTypeTest.java b/src/test/java/net/sf/jsqlparser/parser/LargeObjectTypeTest.java new file mode 100644 index 0000000000..1c7a1a0ce9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LargeObjectTypeTest.java @@ -0,0 +1,89 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.CastExpression; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.select.PlainSelect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class LargeObjectTypeTest { + @ParameterizedTest + @ValueSource(strings = {"CHARACTER LARGE OBJECT", "CHAR LARGE OBJECT", "NCHAR LARGE OBJECT", + "NATIONAL CHARACTER LARGE OBJECT", "BINARY LARGE OBJECT", "character large object"}) + void sharesTypeNameAndLengthAcrossTypeFragmentsCastsAndColumns(String name) throws Exception { + ColDataType type = CCJSqlParserUtil.parseColDataType(name + "(3)"); + assertEquals(name, type.getDataType()); + assertEquals(3, type.getPrecision()); + assertEquals(Arrays.asList("3"), type.getArgumentsStringList()); + assertEquals(name, CCJSqlParserUtil.parseColDataType(name).getDataType()); + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT CAST('abc' AS " + name + "(3)) FROM t"); + CastExpression cast = (CastExpression) select.getSelectItem(0).getExpression(); + assertEquals(name, cast.getColDataType().getDataType()); + assertEquals(3, cast.getColDataType().getPrecision()); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (payload " + name + "(3) NOT NULL)"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD payload " + name + "(3)"); + assertEquals(type.toString(), + CCJSqlParserUtil.parseColDataType(type.toString()).toString()); + } + + @Test + void parsesTheDownstreamConditionWithoutRewritingLiteralContents() throws Exception { + String sql = "(status IN (CAST('hi' AS CHARACTER LARGE OBJECT(2)), " + + "CAST('low' AS CHARACTER LARGE OBJECT(3))))"; + var condition = CCJSqlParserUtil.parseCondExpression(sql, false); + List literals = new ArrayList<>(); + condition.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + literals.add(value.getValue()); + return null; + } + }, null); + assertEquals(Arrays.asList("hi", "low"), literals); + assertEquals(condition.toString(), + CCJSqlParserUtil.parseCondExpression(condition.toString(), false).toString()); + assertSqlCanBeParsedAndDeparsed("SELECT CAST('CHARACTER LARGE OBJECT' " + + "AS CHARACTER LARGE OBJECT(22)) FROM t"); + } + + @Test + void acceptsTokenWhitespaceWithoutChangingIdentifiersOrExistingTypes() throws Exception { + assertEquals("CHARACTER LARGE OBJECT", + CCJSqlParserUtil.parseColDataType("CHARACTER /* comment */ LARGE\nOBJECT") + .getDataType()); + for (String type : Arrays.asList("CHARACTER VARYING(3)", "NATIONAL CHARACTER(3)", + "CLOB", "BLOB", "\"CHARACTER LARGE OBJECT\"", "app.large")) { + ColDataType parsed = CCJSqlParserUtil.parseColDataType(type); + assertEquals(parsed.toString(), + CCJSqlParserUtil.parseColDataType(parsed.toString()).toString()); + } + assertSqlCanBeParsedAndDeparsed("SELECT large, object FROM t"); + } + + @ParameterizedTest + @ValueSource(strings = {"CHARACTER LARGE", "CHARACTER LARGE OTHER", "INT LARGE OBJECT"}) + void rejectsIncompleteOrNonCharacterTypeNames(String type) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parseColDataType(type)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java b/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java new file mode 100644 index 0000000000..6b83c46cb5 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java @@ -0,0 +1,80 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class LeadingCommentsTest { + + @Test + public void blockCommentsBeforeStatementInSourceOrder() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* keep orderby */ /*+ FULL(t) */ SELECT * FROM mytable"); + assertEquals(List.of("/* keep orderby */", "/*+ FULL(t) */"), select.getLeadingComments()); + } + + @ParameterizedTest + @ValueSource(strings = {"\n", "\r\n", "\r"}) + public void lineCommentsExcludeLineTerminators(String lineTerminator) + throws JSQLParserException { + Select select = + (Select) CCJSqlParserUtil.parse("-- paging hint" + lineTerminator + "SELECT 1"); + assertEquals(List.of("-- paging hint"), select.getLeadingComments()); + } + + @Test + public void mixedCommentsRetainExactImagesAndSourceOrder() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* a */ -- b\r\n// c\r/* d */ SELECT 1"); + assertEquals(List.of("/* a */", "-- b", "// c", "/* d */"), select.getLeadingComments()); + } + + @Test + public void commentInsideStatementIsNotLeading() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT /* mid comment */ 1"); + assertEquals(List.of(), select.getLeadingComments()); + } + + @Test + public void statementWithoutComments() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT * FROM mytable"); + assertEquals(List.of(), select.getLeadingComments()); + } + + @Test + public void programmaticStatementHasNoLeadingComments() { + assertEquals(List.of(), new PlainSelect().getLeadingComments()); + } + + @Test + public void toStringDropsLeadingCommentsAsBefore() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* keep orderby */ SELECT * FROM mytable"); + assertFalse(select.toString().contains("/*")); + } + + @Test + public void commentsSplitPerStatement() throws JSQLParserException { + Statements statements = CCJSqlParserUtil + .parseStatements("/* first */ SELECT 1; /* second */ SELECT 2"); + assertEquals(List.of("/* first */"), ((Select) statements.get(0)).getLeadingComments()); + assertEquals(List.of("/* second */"), ((Select) statements.get(1)).getLeadingComments()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/LongChainPredicateTest.java b/src/test/java/net/sf/jsqlparser/parser/LongChainPredicateTest.java new file mode 100644 index 0000000000..9eeceec8d9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LongChainPredicateTest.java @@ -0,0 +1,120 @@ +/*- + * #%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.parser; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; + +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.AllTableColumns; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import org.junit.jupiter.api.Test; + +/** + * Guards the threshold-gated chain-walk memoization in isFunctionAhead() / + * isAllTableColumnsAhead(): chains on both sides of CHAIN_CACHE_THRESHOLD and inside the memoized + * path must keep the exact same AST shapes as the plain walk. The speed-up itself is + * constant-factor only and is carried by the measured numbers, not by a CI timing assertion. + */ +public class LongChainPredicateTest { + + private static String chain(int innerDelimiters, String last) { + // innerDelimiters = dots between s0..sN (chain length before the last part) + StringBuilder sb = new StringBuilder("s0"); + for (int i = 1; i <= innerDelimiters; i++) { + sb.append(".s").append(i); + } + return sb.append(".").append(last).toString(); + } + + private Object firstExpression(String sql) throws Exception { + Select select = (Select) CCJSqlParserUtil.parse(sql); + return ((PlainSelect) select).getSelectItems().get(0).getExpression(); + } + + @Test + void functionOnShortChainFastPath() throws Exception { + // total 8 delimiters: the walk stays inside the plain non-cached path + String sql = "SELECT " + chain(7, "f(1)") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Function, + "8-delimiter chain ending in ( must stay a Function"); + } + + @Test + void functionOnLongChainMemoPath() throws Exception { + // total 9 delimiters: the walk aborts past the threshold and goes through the cache + String sql = "SELECT " + chain(8, "f(1)") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Function, + "9-delimiter chain ending in ( must stay a Function on the memoized path"); + } + + @Test + void columnOnShortChainFastPath() throws Exception { + String sql = "SELECT " + chain(7, "col") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Column); + assertFalse(firstExpression(sql) instanceof Function); + } + + @Test + void columnOnLongChainMemoPath() throws Exception { + String sql = "SELECT " + chain(8, "col") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Column); + assertFalse(firstExpression(sql) instanceof Function); + } + + @Test + void columnOnVeryLongChainMemoPath() throws Exception { + String sql = "SELECT " + chain(40, "col") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Column); + } + + @Test + void allTableColumnsAcrossThreshold() throws Exception { + String shortChain = "SELECT " + chain(7, "*") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(shortChain); + assertTrue(firstExpression(shortChain) instanceof AllTableColumns); + + String longChain = "SELECT " + chain(8, "*") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(longChain); + assertTrue(firstExpression(longChain) instanceof AllTableColumns); + } + + @Test + void oracleOuterJoinColumnPlusOnMemoPath() throws Exception { + // the column(+) exclusion must survive the memoized walk + assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM a, b WHERE " + chain(8, "x(+)") + " = b.x"); + } + + @Test + void veryLongChainsParseInLinearTime() { + // the memoizing walk used to fetch each part with getToken(i), which rescans the + // token list from the start, so one walk over a 10000-part chain took many seconds + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + assertTrue( + firstExpression("SELECT " + chain(10000, "col") + " FROM t") instanceof Column); + assertTrue(firstExpression( + "SELECT " + chain(10000, "*") + " FROM t") instanceof AllTableColumns); + assertTrue(firstExpression( + "SELECT " + chain(10000, "f(1)") + " FROM t") instanceof Function); + }); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/LookaheadCleanupTest.java b/src/test/java/net/sf/jsqlparser/parser/LookaheadCleanupTest.java new file mode 100644 index 0000000000..079c8c1f74 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LookaheadCleanupTest.java @@ -0,0 +1,163 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.*; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.ConnectionDefinition; +import net.sf.jsqlparser.statement.CloudConnectionDefinition; +import net.sf.jsqlparser.statement.export.DBMSDestination; +import org.junit.jupiter.api.Test; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause; +import net.sf.jsqlparser.statement.execute.Execute; +import net.sf.jsqlparser.statement.lock.LockStatement; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.expression.LongValue; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class LookaheadCleanupTest { + @ParameterizedTest + @ValueSource(strings = {"EXA AT conn TABLE t", "ORA AT conn TABLE schemaName.tableName", + "JDBC DRIVER = 'driverName' AT conn TABLE \"t\"", + "EXA AT conn TABLE t (c1, c2) REPLACE TRUNCATE"}) + void dispatchesTableDestinationsWithCompleteConsumption(String sql) throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + DBMSDestination destination = parser.DBMSDestination(); + assertNotNull(destination.getTable()); + assertNull(destination.getStatement()); + assertNotNull(destination.getConnectionDefinition()); + assertEquals(CCJSqlParserConstants.EOF, parser.getToken(1).kind); + assertSqlCanBeParsedAndDeparsed("EXPORT sourceTable INTO " + sql, true, + p -> p.withDialect(Dialect.EXASOL)); + } + + @ParameterizedTest + @ValueSource(strings = {"EXA AT conn STATEMENT 'insert into t values (?)'", + "ORA AT conn STATEMENT 'insert into t values (?)'"}) + void preservesStatementDestinations(String sql) throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + DBMSDestination destination = parser.DBMSDestination(); + assertNull(destination.getTable()); + assertEquals("insert into t values (?)", destination.getStatement().getValue()); + assertEquals(CCJSqlParserConstants.EOF, parser.getToken(1).kind); + assertSqlCanBeParsedAndDeparsed("EXPORT sourceTable INTO " + sql, true, + p -> p.withDialect(Dialect.EXASOL)); + } + + @ParameterizedTest + @ValueSource(strings = {"EXA AT conn TABLE", "ORA AT conn STATEMENT", "EXA AT conn SELECT t"}) + void rejectsIncompleteAndWrongDestinationPrefixes(String sql) { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + assertThrows(ParseException.class, parser::DBMSDestination); + if (sql.endsWith("TABLE")) { + assertEquals(CCJSqlParserConstants.K_TABLE, parser.getToken(0).kind); + assertEquals(CCJSqlParserConstants.EOF, parser.getToken(1).kind); + } + } + + @ParameterizedTest + @ValueSource(strings = {"AT conn", "AT '127.0.0.1'", "AT CLOUD NONE conn", + "AT CLOUD AZURE BLOBSTORAGE conn"}) + void preservesCloudAndOrdinaryConnections(String sql) throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + ConnectionDefinition connection = parser.ConnectionOrCloudConnectionDefinition(); + if (sql.startsWith("AT CLOUD")) { + assertInstanceOf(CloudConnectionDefinition.class, connection); + } else { + assertEquals(ConnectionDefinition.class, connection.getClass()); + } + assertEquals(CCJSqlParserConstants.EOF, parser.getToken(1).kind); + assertSqlCanBeParsedAndDeparsed("EXPORT sourceTable INTO CSV " + sql + " FILE 'file.csv'", + true, p -> p.withDialect(Dialect.EXASOL)); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT id FROM t LIMIT 7 OFFSET 2", + "SELECT id FROM t UNION ALL SELECT id FROM u LIMIT 7 OFFSET 2"}) + void boundedSelectTailsKeepTheirAstAndFollowingStatement(String sql) throws Exception { + Select select = (Select) complete(sql); + assertEquals(7, ((LongValue) select.getLimit().getRowCount()).getValue()); + assertEquals(2, ((LongValue) select.getOffset().getOffset()).getValue()); + assertEquals("SELECT 42", CCJSqlParserUtil.parseStatements(sql + "; SELECT 42").get(1) + .toString()); + } + + @Test + void boundedFetchKeepsItsCount() throws Exception { + Select select = (Select) complete("SELECT id FROM t OFFSET 2 ROWS FETCH NEXT 7 ROWS ONLY"); + assertEquals(2, ((LongValue) select.getOffset().getOffset()).getValue()); + assertEquals(7, ((LongValue) select.getFetch().getExpression()).getValue()); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT a INTO OUTFILE '/tmp/result.txt' FIELDS TERMINATED BY ',' " + + "OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY 'end' FROM t", + "SELECT a FROM t INTO OUTFILE '/tmp/result.txt' COLUMNS TERMINATED BY ',' " + + "OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY 'end'"}) + void boundedOutfileOptionsKeepTheirOrderingAndValues(String sql) throws Exception { + MySqlSelectIntoClause into = ((Select) complete(sql)).getPlainSelect() + .getMySqlSelectIntoClause(); + assertEquals(MySqlSelectIntoClause.Type.OUTFILE, into.getType()); + assertEquals("/tmp/result.txt", into.getFileName().getValue()); + assertEquals(",", into.getFieldsTerminatedBy().getValue()); + assertTrue(into.isFieldsOptionallyEnclosed()); + assertEquals("end", into.getLinesTerminatedBy().getValue()); + } + + @Test + void boundedParenthesesKeepExecutionArgumentsAndRuleActions() throws Exception { + Execute execute = (Execute) complete("CALL p (:a, :b)"); + assertEquals("p", execute.getName()); + assertEquals(2, execute.getExprList().size()); + assertEquals(":a", execute.getExprList().get(0).toString()); + assertEquals(":b", execute.getExprList().get(1).toString()); + CreateRule rule = (CreateRule) complete("CREATE RULE r AS ON INSERT TO t DO " + + "(INSERT INTO log VALUES (9); NOTIFY ch, 'changed')"); + assertEquals(2, rule.getActions().size()); + assertEquals("log", ((Insert) rule.getActions().get(0)).getTable().getName()); + assertEquals("changed", + ((NotifyStatement) rule.getActions().get(1)).getPayload().getValue()); + } + + @ParameterizedTest + @ValueSource(strings = {"LOCK TABLE a IN SHARE MODE WAIT 5", "LOCK a IN SHARE MODE WAIT 5"}) + void boundedTableKeywordKeepsLockTargetAndWait(String sql) throws Exception { + LockStatement lock = (LockStatement) complete(sql); + assertEquals(sql.startsWith("LOCK TABLE"), lock.isUseTableKeyword()); + assertEquals("a", lock.getTable().getName()); + assertEquals("SHARE", lock.getLockMode().getValue()); + assertEquals(5L, lock.getWaitSeconds()); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT id FROM t LIMIT", "SELECT id FROM t OFFSET", + "SELECT id FROM t FETCH NEXT", "CALL p (", "CALL p (:a,)", "LOCK TABLE", + "CREATE RULE r AS ON INSERT TO t DO (NOTIFY ch", "SELECT a FROM t INTO OUTFILE 'x' " + + "LINES TERMINATED BY 'end' FIELDS TERMINATED BY ','"}) + void boundedGuardsRejectIncompleteAndReorderedNeighbors(String sql) { + assertThrows(net.sf.jsqlparser.JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql)); + } + + private static Statement complete(String sql) throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + Statement statement = parser.Statement(); + assertEquals(CCJSqlParserConstants.EOF, parser.getToken(1).kind); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(statement.toString()).toString()); + return statement; + } + +} diff --git a/src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java b/src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java new file mode 100644 index 0000000000..0aa5ee61d0 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java @@ -0,0 +1,223 @@ +/*- + * #%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.parser; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.AllValue; +import net.sf.jsqlparser.expression.BooleanValue; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.OracleNamedFunctionParameter; +import net.sf.jsqlparser.expression.OverlapsCondition; +import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; +import net.sf.jsqlparser.expression.TimeKeyExpression; +import net.sf.jsqlparser.expression.operators.relational.Between; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; +import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class LookaheadDispatchTest { + @ParameterizedTest + @MethodSource("singleTokenExpressions") + void preservesSingleTokenExpressionKinds(String sql, Class type, + String deparsed) throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + Expression expression = parser.PrimaryExpression(); + assertInstanceOf(type, expression); + assertEquals(deparsed, expression.toString()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + Expression parsed = CCJSqlParserUtil.parseExpression(sql, false); + assertInstanceOf(type, parsed); + assertEquals(deparsed, parsed.toString()); + } + + static Stream singleTokenExpressions() { + return Stream.of( + Arguments.of("TRUE", BooleanValue.class, "true"), + Arguments.of("FALSE", BooleanValue.class, "false"), + Arguments.of("ALL", AllValue.class, "ALL"), + Arguments.of("CURRENT", TimeKeyExpression.class, "CURRENT"), + Arguments.of("CURRENT_DATE", TimeKeyExpression.class, "CURRENT_DATE"), + Arguments.of("CURRENT_TIME", TimeKeyExpression.class, "CURRENT_TIME"), + Arguments.of("CURRENT_TIMESTAMP", TimeKeyExpression.class, "CURRENT_TIMESTAMP"), + Arguments.of("CURRENT_TIMEZONE", TimeKeyExpression.class, "CURRENT_TIMEZONE"), + Arguments.of("CURRENT_DATE()", TimeKeyExpression.class, "CURRENT_DATE()")); + } + + @ParameterizedTest + @ValueSource(strings = {"\"TRUE\"", "\"CURRENT_DATE\"", "\"ALL\""}) + void preservesQuotedNames(String name) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT " + name); + assertEquals(name, assertInstanceOf(Column.class, select.getSelectItem(0).getExpression()) + .getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"TRUE", "ALL", "CURRENT_DATE"}) + void honorsInterruptionBeforeSingleTokenExpressions(String sql) { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + parser.interrupted = true; + assertThrows(ParseException.class, parser::PrimaryExpression); + assertEquals(sql, parser.getNextToken().image); + } + + @ParameterizedTest + @MethodSource("conditions") + void preservesConditionAlternatives(String sql, Class type) + throws Exception { + CCJSqlParser conditionParser = CCJSqlParserUtil.newParser(sql); + Expression condition = conditionParser.Condition(); + assertInstanceOf(type, condition); + assertEquals(sql, condition.toString()); + assertEquals(CCJSqlParserConstants.EOF, conditionParser.getNextToken().kind); + + CCJSqlParser sqlConditionParser = CCJSqlParserUtil.newParser(sql); + Expression sqlCondition = sqlConditionParser.SQLCondition(); + assertInstanceOf(type, sqlCondition); + assertEquals(sql, sqlCondition.toString()); + assertEquals(CCJSqlParserConstants.EOF, sqlConditionParser.getNextToken().kind); + + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE " + sql); + assertInstanceOf(type, select.getWhere()); + } + + static Stream conditions() { + return Stream.of( + Arguments.of("a", Column.class), + Arguments.of("exists", Column.class), + Arguments.of("exists.col", Column.class), + Arguments.of("EXISTS (SELECT 1)", ExistsExpression.class), + Arguments.of("EXISTS (1)", ExistsExpression.class), + Arguments.of("a IN (1, 2)", InExpression.class), + Arguments.of("a BETWEEN 1 AND 2", Between.class), + Arguments.of("a IS NULL", IsNullExpression.class), + Arguments.of("(a, b) OVERLAPS (c, d)", OverlapsCondition.class)); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "exists", "exists.col"}) + void preservesComparisons(String column) throws Exception { + String sql = column + " = 1"; + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + EqualsTo condition = assertInstanceOf(EqualsTo.class, parser.Condition()); + assertEquals(column, assertInstanceOf(Column.class, condition.getLeftExpression()) + .getFullyQualifiedName()); + assertEquals(sql, condition.toString()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + sql); + } + + @Test + void preservesOraclePriorPosition() throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser("PRIOR a = b"); + EqualsTo condition = assertInstanceOf(EqualsTo.class, parser.Condition()); + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + condition.getOraclePriorPosition()); + assertEquals("a", assertInstanceOf(Column.class, condition.getLeftExpression()) + .getColumnName()); + assertEquals("b", assertInstanceOf(Column.class, condition.getRightExpression()) + .getColumnName()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t CONNECT BY PRIOR a = b"); + } + + @ParameterizedTest + @ValueSource(strings = {"a IN (", "a BETWEEN 1 AND", "EXISTS (SELECT", + "(a, b) OVERLAPS ("}) + void rejectsMalformedConditions(String sql) { + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Condition()); + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).SQLCondition()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE " + sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"a =", "exists ="}) + void rejectsMalformedComparisons(String sql) { + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Condition()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE " + sql)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void honorsInterruptionBeforeConditionDispatch(boolean sqlCondition) { + CCJSqlParser parser = CCJSqlParserUtil.newParser("1"); + parser.interrupted = true; + assertThrows(ParseException.class, () -> { + if (sqlCondition) { + parser.SQLCondition(); + } else { + parser.Condition(); + } + }); + assertEquals("1", parser.getNextToken().image); + } + + @ParameterizedTest + @CsvSource(value = {"arg|=>", "arg|:=", "OUTER|=>", "OUTER|:=", "\"arg\"|=>", + "\"arg\"|:=", "overlaps|=>", "overlaps|:="}, delimiter = '|') + void preservesNamedParameterKindsInEveryArgumentPosition(String name, String assignment) + throws JSQLParserException { + for (String prefix : new String[] {"", "a.b, "}) { + String sql = "SELECT f(" + prefix + name + " " + assignment + " value_col)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql); + Function function = assertInstanceOf(Function.class, + select.getSelectItem(0).getExpression()); + int index = prefix.isEmpty() ? 0 : 1; + assertEquals(index + 1, function.getParameters().size()); + if (index == 1) { + assertEquals("a.b", assertInstanceOf(Column.class, + function.getParameters().get(0)).getFullyQualifiedName()); + } + Expression argument = function.getParameters().get(index); + Expression value; + if ("=>".equals(assignment)) { + OracleNamedFunctionParameter parameter = + assertInstanceOf(OracleNamedFunctionParameter.class, argument); + assertEquals(name, parameter.getName()); + value = parameter.getExpression(); + } else { + PostgresNamedFunctionParameter parameter = + assertInstanceOf(PostgresNamedFunctionParameter.class, argument); + assertEquals(name, parameter.getName()); + value = parameter.getExpression(); + } + assertEquals("value_col", assertInstanceOf(Column.class, value).getColumnName()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"a.b => 1", "a.b := 1", "1 => 1", "1 := 1", "'a' => 1", + "'a' := 1", "arg =>", "arg :="}) + void rejectsInvalidNamedParametersInEveryArgumentPosition(String argument) { + for (String prefix : new String[] {"", "a.b, "}) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT f(" + prefix + argument + ")")); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/MySqlCommentTest.java b/src/test/java/net/sf/jsqlparser/parser/MySqlCommentTest.java new file mode 100644 index 0000000000..e1fa6d5f93 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/MySqlCommentTest.java @@ -0,0 +1,257 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.SignedExpression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlCommentTest { + private static PlainSelect parse(String sql) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static PlainSelect parse(String sql, int version) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.MYSQL).withMySqlServerVersion(version)); + } + + private static void assertOutput(String expected, Statement statement) throws Exception { + assertEquals(expected, statement.toString()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(expected, output.toString()); + assertEquals(expected, parse(output.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT 1--2", "SELECT 1 --2", "SELECT 1- -2", + "SELECT 1--/*! 2 */"}) + void preservesMinusAndNegativeOperand(String sql) throws Exception { + PlainSelect select = parse(sql); + Subtraction subtraction = + assertInstanceOf(Subtraction.class, select.getSelectItem(0).getExpression()); + assertEquals(1, ((LongValue) subtraction.getLeftExpression()).getValue()); + SignedExpression negative = + assertInstanceOf(SignedExpression.class, subtraction.getRightExpression()); + assertEquals('-', negative.getSign()); + assertEquals(2, ((LongValue) negative.getExpression()).getValue()); + assertOutput("SELECT 1 - -2", select); + } + + @ParameterizedTest + @ValueSource(strings = {" ", "\t", "\r", "\n", "\f", "\u0007", "\u007f"}) + void acceptsWhitespaceOrControlAfterDoubleDash(String separator) throws Exception { + String sql = "SELECT 1--" + separator; + if (!separator.equals("\r") && !separator.equals("\n")) { + sql += "discarded /*! +99 */"; + } + assertOutput("SELECT 1", parse(sql)); + } + + @Test + void keepsLineAndBlockCommentsSeparateFromExecutableSql() throws Exception { + assertOutput("SELECT 1", parse("SELECT 1--")); + assertOutput("SELECT 1 + 2", parse("SELECT 1 -- ignored\n + 2")); + assertOutput("SELECT 1", parse("SELECT 1 /* +99 */")); + assertOutput("SELECT 1", parse("SELECT 1 # /*! +99 */")); + assertOutput("SELECT 1 + 2", parse("SELECT /*! 1 /* inner */ + 2 */")); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT 1 /*! +2 */", "SELECT 1/*!+2*/", + "/*!SELECT 1 + 2*/", "SELECT /*!1*/ /*!+*/ /*!2*/", + "SELECT 1 /*! +\n2 */"}) + void parsesUnversionedCommentContentsAsExpressions(String sql) throws Exception { + PlainSelect select = parse(sql); + Addition addition = + assertInstanceOf(Addition.class, select.getSelectItem(0).getExpression()); + assertEquals(1, ((LongValue) addition.getLeftExpression()).getValue()); + assertEquals(2, ((LongValue) addition.getRightExpression()).getValue()); + assertOutput("SELECT 1 + 2", select); + addition.setRightExpression(new LongValue(3)); + assertOutput("SELECT 1 + 3", select); + } + + @Test + void parsesExecutableSelectModifier() throws Exception { + PlainSelect select = parse("SELECT /*! STRAIGHT_JOIN */ a FROM t"); + assertTrue(select.getMySqlHintStraightJoin()); + assertEquals("a", select.getSelectItem(0).getExpression().toString()); + assertNull(select.getSelectItem(0).getAlias()); + assertOutput("SELECT STRAIGHT_JOIN a FROM t", select); + } + + @Test + void leavesQuotedContentsAndOptimizerHintsIntact() throws Exception { + PlainSelect quoted = parse("SELECT '/*! +2 */ --x', `a--b`, \"/*! +3 */\""); + assertEquals("/*! +2 */ --x", + ((StringValue) quoted.getSelectItem(0).getExpression()).getValue()); + assertEquals("`a--b`", quoted.getSelectItem(1).getExpression().toString()); + assertEquals("/*! +3 */", + ((StringValue) quoted.getSelectItem(2).getExpression()).getValue()); + assertOutput("SELECT '*/', 2", parse("SELECT /*! '*/' */, 2")); + PlainSelect hint = parse("SELECT /*+ BKA(t) */ a FROM t"); + assertNotNull(hint.getOracleHint()); + assertOutput(hint.toString(), hint); + } + + @Test + void respectsExplicitQuoteAndEscapeOverrides() throws Exception { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT /*! \"--x\" */, '/*! text */'", + p -> p.withDialect(Dialect.MYSQL).withDoubleQuotedStrings(false) + .withBackslashEscapeCharacter(false)); + assertInstanceOf(net.sf.jsqlparser.schema.Column.class, + select.getSelectItem(0).getExpression()); + assertEquals("/*! text */", + ((StringValue) select.getSelectItem(1).getExpression()).getValue()); + assertOutput("SELECT 'x\\\'/*! text */'", parse("SELECT /*! 'x\\\'/*! text */' */")); + } + + @Test + void resolvesConditionalCommentsForAnExplicitTarget() throws Exception { + String sql = "SELECT 1 /*!90702 +2 */"; + assertOutput("SELECT 1", parse(sql, 90701)); + assertOutput("SELECT 1 + 2", parse(sql, 90702)); + assertOutput("SELECT 1 + 2", parse(sql, 260700)); + assertOutput("SELECT 1", parse("SELECT 1 /*!260700 +2 */", 90702)); + assertOutput("SELECT 1 + 2", parse("SELECT 1 /*!260700 +2 */", 260700)); + assertOutput("SELECT 1 + 2", parse("SELECT 1 /*!090702\t+2 */", 90702)); + assertOutput("SELECT 1 + 2", parse("SELECT 1 /*!90702+2 */", 90702)); + assertOutput("SELECT 1", parse("SELECT 1 /*!99999 not valid SQL */", 90702)); + } + + @Test + void followsFiveDigitFallbackWhenSixthDigitHasNoFollowingWhitespace() throws Exception { + assertOutput("SELECT 0 + 2", parse("SELECT /*!100000+2*/", 10000)); + assertOutput("SELECT 1234 + 2", parse("SELECT /*!1234+2*/")); + } + + @Test + void requiresVersionWithoutSilentlyFallingBackOrDroppingSql() { + String sql = "SELECT 1 /*!90702 +2 */"; + JSQLParserException exception = assertThrows(JSQLParserException.class, () -> parse(sql)); + assertTrue(exception.getCause().getMessage().contains("withMySqlServerVersion")); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.MYSQL).withUnsupportedStatements())); + assertThrows(TokenMgrException.class, () -> new CCJSqlParser(new StreamProvider( + new StringReader(sql))).withDialect(Dialect.MYSQL).Statement()); + assertThrows(IllegalArgumentException.class, + () -> CCJSqlParserUtil.newParser(sql).withMySqlServerVersion(-1)); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT 1 /*! +2", "SELECT 1 /*!", "SELECT /*! 1", + "SELECT /*! 1 -- closing marker hidden */"}) + void rejectsUnterminatedExecutableComments(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void rejectsNestedExecutableComments() { + assertThrows(JSQLParserException.class, + () -> parse("SELECT /*! 1 /*! +99 */ + 2 */")); + } + + @Test + void sharesLexingAcrossStringReaderInputStreamAndFeatureConfiguration() throws Exception { + String sql = "SELECT 1--2 /*!90702 +3 */"; + String expected = "SELECT 1 - -2 + 3"; + assertOutput(expected, parse(sql, 90702)); + assertOutput(expected, new CCJSqlParser(new StringProvider(sql)).withDialect(Dialect.MYSQL) + .withMySqlServerVersion(90702).Statement()); + assertOutput(expected, new CCJSqlParser(new StreamProvider(new StringReader(sql))) + .withDialect(Dialect.MYSQL).withMySqlServerVersion(90702).Statement()); + assertOutput(expected, CCJSqlParserUtil.newParser(new ByteArrayInputStream( + sql.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8.name()) + .withDialect(Dialect.MYSQL).withMySqlServerVersion(90702).Statement()); + FeatureConfiguration configuration = new FeatureConfiguration() + .setValue(Feature.dialect, Dialect.MYSQL.name()) + .setValue(Feature.mySqlServerVersion, 90702); + assertOutput(expected, + CCJSqlParserUtil.newParser(sql).withConfiguration(configuration).Statement()); + assertEquals("1 - -2 + 3", CCJSqlParserUtil.parseExpression("1--2 /*! +3 */", false, + p -> p.withDialect(Dialect.MYSQL)).toString()); + } + + @Test + void preservesStatementBoundariesAndOriginalTokenPositions() throws Exception { + assertEquals(2, CCJSqlParserUtil.parseStatements("/*! SELECT 1 */; SELECT 2;", + p -> p.withDialect(Dialect.MYSQL)).size()); + assertEquals(2, CCJSqlParserUtil.parseStatements("SELECT 1/*!+2*/; SELECT 3--4;", + p -> p.withDialect(Dialect.MYSQL)).size()); + CCJSqlParser parser = CCJSqlParserUtil.newParser("/*!1*/--2").withDialect(Dialect.MYSQL); + Token one = parser.getNextToken(); + assertEquals("1", one.image); + assertEquals(4, one.beginColumn); + assertEquals(4, one.absoluteBegin); + Token minus = parser.getNextToken(); + assertEquals("-", minus.image); + assertEquals(7, minus.beginColumn); + assertEquals(7, minus.absoluteBegin); + assertEquals("-", parser.getNextToken().image); + assertEquals("2", parser.getNextToken().image); + } + + @Test + void handlesBufferExpansionAndStreamReinitialization() throws Exception { + String sql = "SELECT " + " ".repeat(4090) + "1--2/*!+3*/"; + assertOutput("SELECT 1 - -2 + 3", parse(sql)); + assertOutput("SELECT '" + "x".repeat(9000) + "'", + parse("SELECT /*! '" + "x".repeat(9000) + "' */")); + CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT /*!1*/") + .withDialect(Dialect.MYSQL); + assertEquals("SELECT 1", parser.Statement().toString()); + parser.ReInit(new StringProvider("SELECT 2--3")); + assertEquals("SELECT 2 - -3", parser.Statement().toString()); + } + + @Test + void rejectsLiteralInternalMinusMarkerInEveryDialect() { + String sql = "SELECT 1 " + (char) 2 + " 2"; + for (Dialect dialect : Dialect.values()) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + p -> p.withDialect(dialect)), dialect.name()); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + p -> p.withDialect(dialect).withUnsupportedStatements()), dialect.name()); + } + } + + @Test + void preservesOtherDialectCommentsAndQuotedBodies() throws Exception { + assertEquals("SELECT 1", CCJSqlParserUtil.parse("SELECT 1--2").toString()); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.MYSQL) { + assertEquals("SELECT 1", CCJSqlParserUtil.parse("SELECT 1 /*!90702 +2 */ --3", + p -> p.withDialect(dialect)).toString(), dialect.name()); + } + } + assertEquals("SELECT $x$/*! --2 */$x$", CCJSqlParserUtil.parse( + "SELECT $x$/*! --2 */$x$", p -> p.withDialect(Dialect.POSTGRESQL)).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/OdbcTimestampIntervalTest.java b/src/test/java/net/sf/jsqlparser/parser/OdbcTimestampIntervalTest.java new file mode 100644 index 0000000000..381bbccdac --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/OdbcTimestampIntervalTest.java @@ -0,0 +1,88 @@ +/*- + * #%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.parser; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.DateUnitExpression; +import net.sf.jsqlparser.expression.DateUnitExpression.DateUnit; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +class OdbcTimestampIntervalTest { + @ParameterizedTest + @ValueSource(strings = {"SQL_TSI_FRAC_SECOND", "SQL_TSI_SECOND", "SQL_TSI_MINUTE", + "SQL_TSI_HOUR", "SQL_TSI_DAY", "SQL_TSI_WEEK", "SQL_TSI_MONTH", + "SQL_TSI_QUARTER", "SQL_TSI_YEAR"}) + void recognizesStandardIntervalsForBothFunctions(String unit) throws Exception { + for (String name : List.of("TIMESTAMPADD", "TIMESTAMPDIFF")) { + String sql = "SELECT {fn " + name + "(" + unit + ", 2, travel_date)} FROM t"; + for (boolean complex : new boolean[] {false, true}) { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withAllowComplexParsing(complex)); + Function function = (Function) select.getSelectItem(0).getExpression(); + DateUnitExpression interval = assertInstanceOf(DateUnitExpression.class, + function.getParameters().get(0)); + assertEquals(DateUnit.from(unit), interval.getType()); + assertEquals(unit, interval.toString()); + assertNotNull(interval.getASTNode()); + assertEquals(select.toString(), + CCJSqlParserUtil.parse(select.toString()).toString()); + List columns = new ArrayList<>(); + function.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + columns.add(column.getColumnName()); + return null; + } + }, null); + assertEquals(List.of("travel_date"), columns); + } + } + } + + @ParameterizedTest + @ValueSource(strings = { + "TIMESTAMPADD(SQL_TSI_YEAR, 2, travel_date)", + "{fn other(SQL_TSI_YEAR, 2, travel_date)}", + "{fn schema.TIMESTAMPADD(SQL_TSI_YEAR, 2, travel_date)}", + "{fn TIMESTAMPADD(t.SQL_TSI_YEAR, 2, travel_date)}", + "{fn TIMESTAMPADD(\"SQL_TSI_YEAR\", 2, travel_date)}", + "{fn TIMESTAMPADD(SQL_TSI_UNKNOWN, 2, travel_date)}", + "{fn TIMESTAMPADD(SQL_TSI_YEAR, travel_date)}" + }) + void preservesIdentifiersOutsideTheOdbcIntervalPosition(String expression) throws Exception { + PlainSelect select = + (PlainSelect) CCJSqlParserUtil.parse("SELECT " + expression + " FROM t"); + Function function = (Function) select.getSelectItem(0).getExpression(); + assertInstanceOf(Column.class, function.getParameters().get(0)); + } + + @Test + void handlesCaseAndNestedEscapesWithoutChangingOtherArguments() throws Exception { + String sql = "SELECT {fn timestampdiff(sql_tsi_year, {fn CURDATE()}, SQL_TSI_YEAR)} FROM t"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true, null); + Function function = (Function) select.getSelectItem(0).getExpression(); + assertInstanceOf(DateUnitExpression.class, function.getParameters().get(0)); + assertInstanceOf(Function.class, function.getParameters().get(1)); + assertInstanceOf(Column.class, function.getParameters().get(2)); + assertNull(DateUnit.fromOdbcInterval(null)); + assertNull(DateUnit.fromOdbcInterval("YEAR")); + assertNull(DateUnit.fromOdbcInterval("SQL_TSI_UNKNOWN")); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/OracleVectorTypeTest.java b/src/test/java/net/sf/jsqlparser/parser/OracleVectorTypeTest.java new file mode 100644 index 0000000000..27333e842a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/OracleVectorTypeTest.java @@ -0,0 +1,75 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.Arrays; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class OracleVectorTypeTest { + @ParameterizedTest + @ValueSource(strings = {"*, *", "*, FLOAT32", "3, *", "*, *, DENSE", "*, *, SPARSE", + "*, FLOAT32, SPARSE", "3, *, SPARSE", "3, FLOAT32, *", "*, *, *", "3, FLOAT32", + "3", "1024, FLOAT32, SPARSE"}) + void preservesVectorArguments(String arguments) throws JSQLParserException { + List expected = Arrays.asList(arguments.split(", ")); + for (String name : List.of("VECTOR", "vector")) { + String typeSql = name + "(" + arguments + ")"; + for (ColDataType type : List.of(CCJSqlParserUtil.parseColDataType(typeSql), + CCJSqlParserUtil.parseColDataType(typeSql, + p -> p.withDialect(Dialect.ORACLE)))) { + assertEquals(name, type.getDataType()); + assertEquals(expected, type.getArgumentsStringList()); + assertEquals(name + " (" + arguments + ")", type.toString()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"VECTOR(*, *)", "VECTOR(*, FLOAT32)", "VECTOR(3, *)", + "VECTOR(*, *, SPARSE)", "VECTOR(3, FLOAT32, *)"}) + void roundTripsVectorColumnsBeforeOtherColumns(String typeSql) throws JSQLParserException { + String sql = "CREATE TABLE items (embedding " + typeSql + ", label VARCHAR2(20))"; + for (Statement statement : List.of(TestUtils.assertSqlCanBeParsedAndDeparsed(sql), + CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.ORACLE)))) { + CreateTable table = (CreateTable) statement; + assertEquals(2, table.getColumnDefinitions().size()); + assertEquals("label", table.getColumnDefinitions().get(1).getColumnName()); + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(statement.toString(), buffer.toString()); + CreateTable reparsed = (CreateTable) CCJSqlParserUtil.parse(buffer.toString()); + assertEquals( + table.getColumnDefinitions().get(0).getColDataType().getArgumentsStringList(), + reparsed.getColumnDefinitions().get(0).getColDataType() + .getArgumentsStringList()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"VARCHAR(*)", "NUMERIC(3, *)", "custom_type(*)", + "\"VECTOR\"(*, *)", "VECTOR(*,", "VECTOR(, *)", "VECTOR(*, *) NOT NULL"}) + void rejectsWildcardsOutsideVectorAndIncompleteTypes(String typeSql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parseColDataType(typeSql)); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseColDataType(typeSql, + p -> p.withDialect(Dialect.ORACLE))); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/ParseStatementsFailureTest.java b/src/test/java/net/sf/jsqlparser/parser/ParseStatementsFailureTest.java new file mode 100644 index 0000000000..376d2ad03b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/ParseStatementsFailureTest.java @@ -0,0 +1,81 @@ +/*- + * #%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.parser; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.jupiter.api.Assertions.*; + +class ParseStatementsFailureTest { + @Test + void reportsFailureWhenComplexParsingIsDisabled() { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseStatements("SELECT FROM", + parser -> parser.withAllowComplexParsing(false))); + assertNotNull(exception.getCause()); + } + + @Test + void reportsFailureWhenNestingPreventsRetry() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseStatements("SELECT (( FROM", + parser -> parser.withAllowComplexParsing(true).withAllowedNestingDepth(0))); + } + + @Test + void leavesCallerExecutorUsableAfterFailure() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseStatements("SELECT FROM", executor, + parser -> parser.withAllowComplexParsing(false))); + assertFalse(executor.isShutdown()); + assertEquals(2, CCJSqlParserUtil.parseStatements("SELECT 1; SELECT 2", executor, + parser -> parser.withAllowComplexParsing(false)).size()); + } finally { + executor.shutdownNow(); + } + } + + @Test + void reportsTimeoutWhenComplexParsingIsDisabled() { + ExecutorService executor = Executors.newSingleThreadExecutor(); + CountDownLatch release = new CountDownLatch(1); + executor.submit(() -> release.await(30, TimeUnit.SECONDS)); + try { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseStatements("SELECT 1", executor, + parser -> parser.withAllowComplexParsing(false).withTimeOut(50))); + assertInstanceOf(TimeoutException.class, exception.getCause()); + } finally { + release.countDown(); + executor.shutdownNow(); + } + } + + @Test + void returnsEmptyListsAndPreservesUnsupportedStatements() throws Exception { + assertTrue(CCJSqlParserUtil.parseStatements((String) null).isEmpty()); + assertTrue(CCJSqlParserUtil.parseStatements("").isEmpty()); + assertInstanceOf(UnsupportedStatement.class, + CCJSqlParserUtil.parseStatements("SELECT 1; WHATEVER !", + parser -> parser.withAllowComplexParsing(false) + .withUnsupportedStatements(true)) + .get(1)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/ParserExecutionTest.java b/src/test/java/net/sf/jsqlparser/parser/ParserExecutionTest.java new file mode 100644 index 0000000000..09ad06b042 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/ParserExecutionTest.java @@ -0,0 +1,142 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.Executors; +import net.sf.jsqlparser.JSQLParserException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ParserExecutionTest { + private static class QueuedExecutor extends AbstractExecutorService { + Future task; + int submissions; + boolean shutdown; + + @Override + public void execute(Runnable command) { + task = (Future) command; + submissions++; + } + + @Override + public void shutdown() { + shutdown = true; + } + + @Override + public List shutdownNow() { + shutdown(); + return List.of(); + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public boolean isTerminated() { + return shutdown; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return shutdown; + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void cancelsTheSubmittedTaskAndPreservesCallerInterruption(boolean multiple) { + QueuedExecutor executor = new QueuedExecutor(); + CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT 1"); + Thread.currentThread().interrupt(); + try { + JSQLParserException error = assertThrows(JSQLParserException.class, () -> { + if (multiple) { + CCJSqlParserUtil.parseStatements(parser, executor); + } else { + CCJSqlParserUtil.parseStatement(parser, executor); + } + }); + assertInstanceOf(InterruptedException.class, error.getCause()); + assertTrue(Thread.currentThread().isInterrupted()); + assertTrue(parser.interrupted); + assertTrue(executor.task.isCancelled()); + assertFalse(executor.isShutdown()); + } finally { + Thread.interrupted(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void doesNotRetryInterruptedConvenienceCalls(boolean multiple) { + QueuedExecutor executor = new QueuedExecutor(); + Thread.currentThread().interrupt(); + try { + JSQLParserException error = assertThrows(JSQLParserException.class, () -> { + if (multiple) { + CCJSqlParserUtil.parseStatements("SELECT 1", executor, null); + } else { + CCJSqlParserUtil.parse("SELECT 1", executor, null); + } + }); + assertInstanceOf(InterruptedException.class, error.getCause()); + assertEquals(1, executor.submissions); + assertTrue(executor.task.isCancelled()); + assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void timeoutCancelsOneTaskWithoutRetryingOrInterruptingTheCaller(boolean multiple) { + QueuedExecutor executor = new QueuedExecutor(); + JSQLParserException error = assertThrows(JSQLParserException.class, () -> { + if (multiple) { + CCJSqlParserUtil.parseStatements("SELECT 1", executor, + parser -> parser.withTimeOut(0)); + } else { + CCJSqlParserUtil.parse("SELECT 1", executor, parser -> parser.withTimeOut(0)); + } + }); + assertInstanceOf(TimeoutException.class, error.getCause()); + assertEquals(1, executor.submissions); + assertTrue(executor.task.isCancelled()); + assertFalse(Thread.currentThread().isInterrupted()); + assertFalse(executor.isShutdown()); + } + + @Test + void successfulAndInvalidParsingKeepTheCallerExecutorUsable() throws Exception { + var executor = Executors.newSingleThreadExecutor(); + try { + assertEquals("SELECT 1", CCJSqlParserUtil.parse("SELECT 1", executor, null).toString()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT FROM", executor, null)); + assertEquals("SELECT 2;\n", + CCJSqlParserUtil.parseStatements("SELECT 2", executor, null).toString()); + assertFalse(executor.isShutdown()); + } finally { + executor.shutdownNow(); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/PostgreSqlHashOperatorTest.java b/src/test/java/net/sf/jsqlparser/parser/PostgreSqlHashOperatorTest.java new file mode 100644 index 0000000000..5644161928 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/PostgreSqlHashOperatorTest.java @@ -0,0 +1,99 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.*; + +import net.sf.jsqlparser.expression.JsonExpression; +import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseRightShift; +import net.sf.jsqlparser.expression.operators.relational.Intersects; +import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlHashOperatorTest { + private static PlainSelect parse(String sql) throws Exception { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @ValueSource(strings = {"js#>>'{a,b,1}'", "js #>> '{a,b,1}'", "js#>> '{a,b,1}'", + "t.js#>>'{a,b,1}'", "\"js#\"#>>'{a,b,1}'", "js/*comment*/#>>'{a,b,1}'"}) + void preservesJsonPrecedenceWithOrWithoutSpacesIssue2163(String expression) throws Exception { + PlainSelect select = parse("SELECT * FROM t WHERE " + expression + " <> 'bar'"); + NotEqualsTo condition = (NotEqualsTo) select.getWhere(); + assertInstanceOf(JsonExpression.class, condition.getLeftExpression()); + assertTrue(select.toString().contains(" #>> ")); + StringBuilder output = new StringBuilder(); + select.accept(new StatementDeParser(output)); + assertEquals(select.toString(), output.toString()); + assertEquals(select.toString(), parse(output.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"js#>'{a}'", "js#>'{a}'#>>'{b}'"}) + void recognizesOtherHashJsonOperators(String expression) throws Exception { + PlainSelect select = parse("SELECT " + expression + " FROM t"); + assertInstanceOf(JsonExpression.class, select.getSelectItem(0).getExpression()); + assertEquals(select.toString(), parse(select.toString()).toString()); + } + + @Test + void splitsHashXorAndPreservesQuotedIdentifiersAndLiterals() throws Exception { + PlainSelect select = parse("SELECT a#b, a#2, \"a#b\", '#>>', $$a#b$$ FROM t"); + assertInstanceOf(Intersects.class, select.getSelectItem(0).getExpression()); + assertInstanceOf(Intersects.class, select.getSelectItem(1).getExpression()); + assertEquals("\"a#b\"", ((Column) select.getSelectItem(2).getExpression()).getColumnName()); + assertEquals(select.toString(), parse(select.toString()).toString()); + } + + @Test + void leavesLegacyAndSqlServerNamesAndMysqlCommentsIntact() throws Exception { + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.POSTGRESQL && dialect != Dialect.MYSQL + && dialect != Dialect.MARIADB && dialect != Dialect.BIGQUERY) { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a#b FROM #temp", + p -> p.withDialect(dialect)); + assertEquals("a#b", + ((Column) select.getSelectItem(0).getExpression()).getColumnName()); + } + } + PlainSelect legacy = (PlainSelect) CCJSqlParserUtil.parse("SELECT js#>>'{a}' FROM t"); + assertInstanceOf(BitwiseRightShift.class, legacy.getSelectItem(0).getExpression()); + PlainSelect mysql = (PlainSelect) CCJSqlParserUtil.parse("SELECT a#comment\nFROM t", + p -> p.withDialect(Dialect.MYSQL)); + assertEquals("SELECT a FROM t", mysql.toString()); + } + + @Test + void keepsTokenOffsetsAfterSplittingAndAcrossStatements() throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser("js#>>'{}'") + .withDialect(Dialect.POSTGRESQL); + Token name = parser.getNextToken(); + Token operator = parser.getNextToken(); + assertEquals("js", name.image); + assertEquals(1, name.beginColumn); + assertEquals(2, name.endColumn); + assertEquals(1, name.absoluteBegin); + assertEquals(3, name.absoluteEnd); + assertEquals("#>>", operator.image); + assertEquals(3, operator.beginColumn); + assertEquals(5, operator.endColumn); + assertEquals(3, operator.absoluteBegin); + assertEquals(6, operator.absoluteEnd); + assertEquals(2, CCJSqlParserUtil.parseStatements("SELECT js#>>'{}' FROM t; SELECT 1;", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/PostgreSqlJsonPathOperatorTest.java b/src/test/java/net/sf/jsqlparser/parser/PostgreSqlJsonPathOperatorTest.java new file mode 100644 index 0000000000..b49a8dbbeb --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/PostgreSqlJsonPathOperatorTest.java @@ -0,0 +1,171 @@ +/*- + * #%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.parser; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.JsonExpression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.arithmetic.Concat; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; +import net.sf.jsqlparser.expression.operators.relational.JsonOperator; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class PostgreSqlJsonPathOperatorTest { + private PlainSelect parse(String sql) throws Exception { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT '{\"a\":1}'::jsonb #- '{a}'", + "SELECT doc#-'{a}' FROM events", + "SELECT doc/*comment*/#-ARRAY['a', '0'] FROM events", + "SELECT doc@?'$.a[*] ? (@ > 2)' FROM events", + "SELECT NULL::jsonb @? '$.a'", + "SELECT doc #- '{a}' #- '{b}' FROM events" + }) + void buildsJsonOperatorsAndPreservesOutput(String sql) throws Exception { + PlainSelect select = parse(sql); + JsonOperator operator = + assertInstanceOf(JsonOperator.class, select.getSelectItem(0).getExpression()); + assertEquals(sql.contains("@?") ? "@?" : "#-", operator.getStringExpression()); + StringBuilder output = new StringBuilder(); + select.accept(new StatementDeParser(output), null); + assertEquals(select.toString(), output.toString()); + assertEquals(select.toString(), parse(output.toString()).toString()); + assertFalse(output.toString().contains("# -")); + } + + @Test + void preservesAssociativityAndComparisonPrecedence() throws Exception { + PlainSelect select = parse("SELECT doc #- '{a}' #- '{b}' = '{}'::jsonb FROM events"); + EqualsTo comparison = + assertInstanceOf(EqualsTo.class, select.getSelectItem(0).getExpression()); + JsonOperator deletion = + assertInstanceOf(JsonOperator.class, comparison.getLeftExpression()); + assertEquals("#-", deletion.getStringExpression()); + assertInstanceOf(JsonOperator.class, deletion.getLeftExpression()); + IsBooleanExpression predicate = assertInstanceOf(IsBooleanExpression.class, + parse("SELECT doc #- '{a}' @? '$.b' IS TRUE FROM events") + .getSelectItem(0).getExpression()); + JsonOperator exists = assertInstanceOf(JsonOperator.class, predicate.getLeftExpression()); + assertEquals("@?", exists.getStringExpression()); + assertInstanceOf(JsonOperator.class, exists.getLeftExpression()); + } + + @Test + void sharesLeftAssociativityWithPostgresqlConcatenation() throws Exception { + Concat concat = assertInstanceOf(Concat.class, + parse("SELECT doc #- '{a}' || other FROM events").getSelectItem(0).getExpression()); + assertInstanceOf(JsonOperator.class, concat.getLeftExpression()); + JsonOperator deletion = assertInstanceOf(JsonOperator.class, + parse("SELECT doc || other #- '{a}' FROM events").getSelectItem(0).getExpression()); + assertInstanceOf(Concat.class, deletion.getLeftExpression()); + } + + @Test + void composesAccessAndPathOperatorsInBothParserModes() throws Exception { + for (boolean complex : List.of(false, true)) { + for (String operator : List.of("->", "#>")) { + CCJSqlParser parser = CCJSqlParserUtil.newParser( + "SELECT doc " + operator + " '{a}' #- '{b}' FROM events") + .withDialect(Dialect.POSTGRESQL).withAllowComplexParsing(complex); + JsonOperator deletion = assertInstanceOf(JsonOperator.class, + ((PlainSelect) parser.Statement()).getSelectItem(0).getExpression()); + JsonExpression access = + assertInstanceOf(JsonExpression.class, deletion.getLeftExpression()); + assertInstanceOf(StringValue.class, access.getIdentList().get(0).getKey()); + } + CCJSqlParser parser = CCJSqlParserUtil.newParser( + "SELECT doc #- '{a}' ->> 'b' = 'x' FROM events") + .withDialect(Dialect.POSTGRESQL).withAllowComplexParsing(complex); + EqualsTo comparison = assertInstanceOf(EqualsTo.class, + ((PlainSelect) parser.Statement()).getSelectItem(0).getExpression()); + JsonExpression access = + assertInstanceOf(JsonExpression.class, comparison.getLeftExpression()); + assertInstanceOf(JsonOperator.class, access.getExpression()); + assertInstanceOf(StringValue.class, access.getIdentList().get(0).getKey()); + } + } + + @Test + void distinguishesPathOperatorFromParametersAndVisitsOperands() throws Exception { + PlainSelect select = parse("SELECT doc @? ? FROM events WHERE doc #- ? = ?::jsonb"); + JsonOperator exists = (JsonOperator) select.getSelectItem(0).getExpression(); + assertEquals(1, ((JdbcParameter) exists.getRightExpression()).getIndex()); + EqualsTo comparison = (EqualsTo) select.getWhere(); + JsonOperator deletion = (JsonOperator) comparison.getLeftExpression(); + assertEquals(2, ((JdbcParameter) deletion.getRightExpression()).getIndex()); + + List visited = new ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + visited.add(value.getValue()); + return getBuilder().append("'changed'"); + } + }; + StringBuilder output = new StringBuilder(); + parse("SELECT doc #- '{a}' @? '$.b' FROM events") + .accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(List.of("{a}", "$.b"), visited); + assertEquals("SELECT doc #- 'changed' @? 'changed' FROM events", output.toString()); + } + + @Test + void retainsExistingHashAndJsonOperators() throws Exception { + for (String sql : List.of("SELECT 5 # -1", "SELECT doc #> '{a}' FROM events", + "SELECT doc #>> '{a}' FROM events", "SELECT doc ? 'a' FROM events", + "SELECT doc @@ '$.a > 2' FROM events")) { + assertEquals(parse(sql).toString(), parse(parse(sql).toString()).toString()); + } + Expression expression = parse("SELECT doc #- '{a}' FROM events") + .getSelectItem(0).getExpression(); + ((JsonOperator) expression).setRightExpression(new StringValue("{b}")); + assertEquals("doc #- '{b}'", expression.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT '{}'::jsonb #-", "SELECT '{}'::jsonb @?", + "SELECT '{}'::jsonb @? , 1"}) + void requiresRightOperand(String sql) { + assertThrows(net.sf.jsqlparser.JSQLParserException.class, () -> parse(sql)); + } + + @Test + void retainsAtSignIdentifierBehaviorInOtherDialects() throws Exception { + for (Dialect dialect : List.of(Dialect.SQLSERVER, Dialect.MYSQL)) { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT name@host FROM users", + p -> p.withDialect(dialect)); + assertEquals("name@host", select.getSelectItem(0).getExpression().toString()); + } + CCJSqlParser parser = CCJSqlParserUtil.newParser("doc@?'$.a'") + .withDialect(Dialect.POSTGRESQL); + assertEquals("doc", parser.getNextToken().image); + Token operator = parser.getNextToken(); + assertEquals("@?", operator.image); + assertEquals(4, operator.beginColumn); + assertEquals(5, operator.endColumn); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/PostgreSqlNumericTypeTest.java b/src/test/java/net/sf/jsqlparser/parser/PostgreSqlNumericTypeTest.java new file mode 100644 index 0000000000..41148ef5c6 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/PostgreSqlNumericTypeTest.java @@ -0,0 +1,119 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.CastExpression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlNumericTypeTest { + static Stream types() { + return Stream.of(Arguments.of("numeric(2,-3)", 2, -3), Arguments.of("decimal(2,-3)", 2, -3), + Arguments.of("dec(2,-3)", 2, -3), Arguments.of("numeric(2,-1)", 2, -1), + Arguments.of("numeric(2,0)", 2, 0), Arguments.of("numeric(3,5)", 3, 5), + Arguments.of("numeric(1000,-1000)", 1000, -1000), + Arguments.of("numeric(1,1000)", 1, 1000), Arguments.of("numeric(2,- 3)", 2, -3), + Arguments.of("numeric(2,/* scale */-3)", 2, -3), + Arguments.of("pg_catalog.numeric(2,-3)", 2, -3), + Arguments.of("\"numeric\"(2,-3)", 2, -3), Arguments.of("numeric(2,-3)[]", 2, -3), + Arguments.of("numeric(2,-3)[][]", 2, -3), Arguments.of("numeric(2)", 2, null), + Arguments.of("numeric", null, null), Arguments.of("varchar(8)", 8, null), + Arguments.of("timestamp(3) with time zone", 3, null)); + } + + @ParameterizedTest + @MethodSource("types") + void exposesParametersInFragmentsDdlAndCasts(String sqlType, Integer precision, Integer scale) + throws Exception { + ColDataType type = CCJSqlParserUtil.parseColDataType(sqlType, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertParameters(type, precision, scale); + assertParameters(CCJSqlParserUtil.parseColDataType(type.toString()), precision, scale); + for (String sql : List.of("CREATE TABLE t (c " + sqlType + ")", + "SELECT CAST(NULL AS " + sqlType + ")", "SELECT NULL::" + sqlType)) { + Statement statement = parse(sql); + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(statement.toString(), buffer.toString()); + for (Statement tree : List.of(statement, parse(buffer.toString()))) { + ColDataType actual = tree instanceof CreateTable + ? ((CreateTable) tree).getColumnDefinitions().get(0).getColDataType() + : ((PlainSelect) tree).getSelectItem(0).getExpression(CastExpression.class) + .getColDataType(); + assertParameters(actual, precision, scale); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ALTER COLUMN c TYPE numeric(2,-3)", + "CREATE DOMAIN rounded AS numeric(2,-3)", + "CREATE TYPE composite_t AS (c numeric(2,-3))", + "CREATE TABLE t(c numeric(2,-3) DEFAULT 12345::numeric(2,-3))", + "CREATE TABLE t(c numeric(2,-3) GENERATED ALWAYS AS (12345::numeric(2,-3)) STORED)"}) + void supportsSharedTypeGrammarInOtherDdl(String sql) throws Exception { + Statement statement = parse(sql); + assertEquals(statement.toString(), parse(statement.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"numeric(2,-)", "numeric(2,+3)", "numeric(+2,3)", "numeric(2,-3.5)", + "numeric(2,--3)", "numeric(2,-3) NOT NULL"}) + void rejectsMalformedFragmentsAndTrailingInput(String type) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseColDataType(type, + p -> p.withDialect(Dialect.POSTGRESQL))); + } + + @Test + void distinguishesNegativeScaleFromOmittedScaleWithoutChangingLegacyConstructor() { + ColDataType negative = ColDataType.fromNumericParameters("numeric", 2, -1); + assertEquals(-1, negative.getScale()); + assertEquals("numeric (2, -1)", negative.toString()); + ColDataType legacy = new ColDataType("numeric", 2, -1); + assertNull(legacy.getScale()); + assertEquals("numeric (2)", legacy.toString()); + ColDataType boxedLegacy = new ColDataType("numeric", 2, Integer.valueOf(-1)); + assertNull(boxedLegacy.getScale()); + } + + @Test + void preservesSharedOracleNumberSyntaxAndDefaultConfiguration() throws Exception { + for (ColDataType type : List.of(CCJSqlParserUtil.parseColDataType("NUMBER(5,-2)"), + CCJSqlParserUtil.parseColDataType("NUMBER(5,-2)", + p -> p.withDialect(Dialect.ORACLE)))) { + assertParameters(type, 5, -2); + } + } + + private static Statement parse(String sql) throws Exception { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertParameters(ColDataType type, Integer precision, Integer scale) { + assertEquals(precision, type.getPrecision(), type.toString()); + assertEquals(scale, type.getScale(), type.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/PostgreSqlQuotedIdentifierTest.java b/src/test/java/net/sf/jsqlparser/parser/PostgreSqlQuotedIdentifierTest.java new file mode 100644 index 0000000000..1252fe3492 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/PostgreSqlQuotedIdentifierTest.java @@ -0,0 +1,169 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlQuotedIdentifierTest { + static Stream names() { + return Stream.of(Arguments.of("plain", null, "plain"), + Arguments.of("\"normal\"", null, "\"normal\""), + Arguments.of("\"a.b\"", null, "\"a.b\""), + Arguments.of("public.\"a.b\"", "public", "\"a.b\""), + Arguments.of("\"s.p\".\"a.b\"", "\"s.p\"", "\"a.b\""), + Arguments.of("\"a\"\"b.c\"", null, "\"a\"\"b.c\""), + Arguments.of("\"한.글\"", null, "\"한.글\""), + Arguments.of("\"a.\"", null, "\"a.\""), + Arguments.of("\".b\"", null, "\".b\""), + Arguments.of("\"a..b\"", null, "\"a..b\""), + Arguments.of("\".\"", null, "\".\"")); + } + + @ParameterizedTest + @MethodSource("names") + void preservesObjectNamesAcrossStatements(String name, String schema, String leaf) + throws Exception { + for (String sql : List.of("DROP INDEX " + name, "DROP TABLE IF EXISTS " + name, + "CREATE TABLE " + name + " (c int)", "SELECT c FROM " + name, + "INSERT INTO " + name + " (c) VALUES (1)", "UPDATE " + name + " SET c = 1", + "DELETE FROM " + name, "ALTER TABLE " + name + " ADD COLUMN d int", + "COMMENT ON TABLE " + name + " IS $tag$body$tag$")) { + for (Statement statement : roundTrip(sql)) { + Table table = tableOf(statement); + assertEquals(leaf, table.getName(), sql); + assertEquals(schema, table.getSchemaName(), sql); + assertNull(table.getDatabaseName(), sql); + assertEquals(name, table.getFullyQualifiedName(), sql); + assertEquals(table.getNameParts(), table.clone().getNameParts(), sql); + assertEquals(name, table.clone().getFullyQualifiedName(), sql); + Table reference = new Table("ref").setResolvedTable(table); + assertNotSame(table, reference.getResolvedTable()); + assertEquals(table.getNameParts(), reference.getResolvedTable().getNameParts()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"c", "\"a.b\"", "\"a\"\"b.c\"", "\"한.글\"", "\"a.\"", "\".\""}) + void preservesColumnAndQualifierComponents(String name) throws Exception { + for (String ref : List.of(name, "\"t.x\"." + name, "\"s.p\".\"t.x\"." + name)) { + for (Statement statement : roundTrip("SELECT " + ref + " FROM \"s.p\".\"t.x\"")) { + Column column = + ((PlainSelect) statement).getSelectItem(0).getExpression(Column.class); + assertEquals(name, column.getColumnName()); + assertEquals(ref, column.getFullyQualifiedName()); + if (ref.equals(name)) { + assertNull(column.getTable()); + } else { + assertEquals("\"t.x\"", column.getTable().getName()); + } + } + } + for (Statement statement : roundTrip( + "COMMENT ON COLUMN \"t.x\"." + name + " IS $$body$$")) { + Column column = ((Comment) statement).getColumn(); + assertEquals(name, column.getColumnName()); + assertEquals("\"t.x\"", column.getTable().getName()); + } + for (Statement statement : roundTrip("INSERT INTO \"t.x\" (" + name + ") VALUES (1)")) { + assertEquals(name, ((Insert) statement).getColumns().get(0).getColumnName()); + } + } + + @Test + void preservesEveryNameInDropList() throws Exception { + for (Statement statement : roundTrip( + "DROP INDEX IF EXISTS \"a.b\", public.\"c.d\" CASCADE")) { + Drop drop = (Drop) statement; + assertEquals("\"a.b\"", drop.getNames().get(0).getName()); + assertNull(drop.getNames().get(0).getSchemaName()); + assertEquals("\"c.d\"", drop.getNames().get(1).getName()); + assertEquals("public", drop.getNames().get(1).getSchemaName()); + } + } + + @Test + void keepsLegacyBigQuerySplittingAndParserIsolation() throws Exception { + PlainSelect postgres = (PlainSelect) parse("SELECT \"a.b\" FROM \"s.t\""); + assertEquals("\"a.b\"", + postgres.getSelectItem(0).getExpression(Column.class).getColumnName()); + for (boolean explicitDialect : List.of(false, true)) { + PlainSelect bigquery = (PlainSelect) CCJSqlParserUtil.parse("SELECT * FROM `d.s.t`", + parser -> { + if (explicitDialect) { + parser.withDialect(Dialect.BIGQUERY); + } + }); + Table table = (Table) bigquery.getFromItem(); + assertEquals("d", table.getUnquotedDatabaseName()); + assertEquals("s", table.getUnquotedSchemaName()); + assertEquals("t", table.getUnquotedName()); + } + } + + private static Statement parse(String sql) throws Exception { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static List roundTrip(String sql) throws Exception { + Statement statement = parse(sql); + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(statement.toString(), buffer.toString()); + return List.of(statement, parse(statement.toString()), parse(buffer.toString())); + } + + private static Table tableOf(Statement statement) { + if (statement instanceof Drop) { + return ((Drop) statement).getName(); + } + if (statement instanceof CreateTable) { + return ((CreateTable) statement).getTable(); + } + if (statement instanceof PlainSelect) { + return (Table) ((PlainSelect) statement).getFromItem(); + } + if (statement instanceof Insert) { + return ((Insert) statement).getTable(); + } + if (statement instanceof Update) { + return ((Update) statement).getTable(); + } + if (statement instanceof Delete) { + return ((Delete) statement).getTable(); + } + if (statement instanceof Alter) { + return ((Alter) statement).getTable(); + } + return ((Comment) statement).getTable(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/schema/ColumnTest.java b/src/test/java/net/sf/jsqlparser/schema/ColumnTest.java index 4c8469429e..c5db20f74c 100644 --- a/src/test/java/net/sf/jsqlparser/schema/ColumnTest.java +++ b/src/test/java/net/sf/jsqlparser/schema/ColumnTest.java @@ -9,17 +9,34 @@ */ package net.sf.jsqlparser.schema; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.PlainSelect; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.stream.Stream; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.test.TestUtils.assertStatementCanBeDeparsedAs; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; /** * * @author tw */ +@ResourceLock(Resources.SYSTEM_PROPERTIES) public class ColumnTest { @Test @@ -54,4 +71,202 @@ public void testConstructorNamePartsAndDelimiters() { assertThat(table.getNamePartDelimiters()).containsExactly(".", ":"); } + @ParameterizedTest + @MethodSource("qualifiedQuotedNames") + void testQualifiedQuotedNames(String name, List tableParts, String columnName) { + assertColumn(new Column(name), name, tableParts, columnName); + Column column = new Column(); + column.setName(name, true); + assertColumn(column, name, tableParts, columnName); + assertColumn(new Column().withColumnName(name), name, tableParts, columnName); + } + + @ParameterizedTest + @MethodSource("separatedQuotedNames") + void testQuotedNameParts(String name, List tableParts, String columnName) { + List nameParts = new ArrayList<>(tableParts); + Collections.reverse(nameParts); + nameParts.add(columnName); + assertColumn(new Column(nameParts), name, tableParts, columnName); + assertColumn(new Column(nameParts, Collections.nCopies(nameParts.size() - 1, ".")), + name, tableParts, columnName); + } + + @ParameterizedTest + @MethodSource("parsedQualifiedQuotedNames") + void testParsedQualifiedQuotedNames(String name, List tableParts, String columnName) + throws JSQLParserException { + String sql = "SELECT " + name + " FROM t1"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withSquareBracketQuotation(true)); + assertColumn(select.getSelectItem(0).getExpression(Column.class), name, tableParts, + columnName); + PlainSelect reparsed = (PlainSelect) CCJSqlParserUtil.parse(select.toString(), + parser -> parser.withSquareBracketQuotation(true)); + assertColumn(reparsed.getSelectItem(0).getExpression(Column.class), name, tableParts, + columnName); + } + + @ParameterizedTest + @MethodSource("wholeQuotedNames") + void testWholeQuotedDottedNames(String name, String expected, List tableParts, + String columnName) throws JSQLParserException { + assertColumn(new Column(name), expected, tableParts, columnName); + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT " + name + " FROM t1", + parser -> parser.withSquareBracketQuotation(true)); + assertColumn(select.getSelectItem(0).getExpression(Column.class), expected, tableParts, + columnName); + assertStatementCanBeDeparsedAs(select, "SELECT " + expected + " FROM t1"); + } + + @ParameterizedTest + @ValueSource(strings = {"id", "`id`", "\"id\"", "[id]"}) + void testUnqualifiedNames(String name) { + Column column = new Column(name); + assertEquals(name, column.getColumnName()); + assertEquals(name, column.toString()); + assertEquals("id", column.getUnquotedColumnName()); + assertNull(column.getTable()); + } + + @ParameterizedTest + @MethodSource("legacyQualifiedTableNames") + void testLegacyQualifiedTableNames(String tableName, String expected, + List tableParts) { + assertColumn(new Column(List.of(tableName, "id")), expected, tableParts, "id"); + assertColumn(new Column(List.of(tableName, "id"), List.of(".")), expected, + tableParts, "id"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testBigQueryQualifiedTableColumn(boolean explicitDialect) throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT `project.dataset.table`.id FROM `project.dataset.table`", parser -> { + if (explicitDialect) { + parser.withDialect(Dialect.BIGQUERY); + } + }); + Column column = select.getSelectItem(0).getExpression(Column.class); + assertColumn(column, "\"project\".\"dataset\".\"table\".id", + List.of("\"table\"", "\"dataset\"", "\"project\""), "id"); + assertEquals("project", column.getUnquotedCatalogName()); + assertEquals("dataset", column.getUnquotedSchemaName()); + assertEquals("table", column.getUnquotedTableName()); + assertEquals(select.getFromItem(Table.class).getNameParts(), + column.getTable().getNameParts()); + assertStatementCanBeDeparsedAs(select, + "SELECT \"project\".\"dataset\".\"table\".id FROM \"project\".\"dataset\".\"table\""); + } + + @ParameterizedTest + @ValueSource(strings = {"t1.`id`", "`t1.id`", "\"t1.id\"", "[t1.id]", "s.t1.id"}) + void testExplicitlyDisabledSplitting(String name) { + Column column = new Column(); + column.setName(name, false); + assertEquals(name, column.getColumnName()); + assertEquals(name, column.toString()); + assertNull(column.getTable()); + } + + @ParameterizedTest + @ValueSource(strings = {"false", "OFF", "0"}) + void testSystemPropertyDisablesSplitting(String value) { + String previous = System.getProperty("SPLIT_NAMES_ON_DELIMITER"); + try { + System.setProperty("SPLIT_NAMES_ON_DELIMITER", value); + for (String name : List.of("t1.`id`", "`t1.id`", "\"t1.id\"", "[t1.id]", "s.t1.id")) { + Column column = new Column(name); + assertEquals(name, column.getColumnName()); + assertEquals(name, column.toString()); + assertNull(column.getTable()); + } + } finally { + if (previous == null) { + System.clearProperty("SPLIT_NAMES_ON_DELIMITER"); + } else { + System.setProperty("SPLIT_NAMES_ON_DELIMITER", previous); + } + } + } + + private static void assertColumn(Column column, String name, List tableParts, + String columnName) { + assertEquals(name, column.toString()); + assertEquals(name, column.getFullyQualifiedName()); + assertEquals(columnName, column.getColumnName()); + assertThat(column.getTable().getNameParts()).containsExactlyElementsOf(tableParts); + assertEquals(tableParts.get(0), column.getTableName()); + assertEquals(tableParts.size() > 1 ? tableParts.get(1) : null, column.getSchemaName()); + assertEquals(tableParts.size() > 2 ? tableParts.get(2) : null, column.getCatalogName()); + } + + static Stream qualifiedQuotedNames() { + return Stream.of( + Arguments.of("t1.`id`", List.of("t1"), "`id`"), + Arguments.of("t1.\"id\"", List.of("t1"), "\"id\""), + Arguments.of("t1.[id]", List.of("t1"), "[id]"), + Arguments.of("`t1`.id", List.of("`t1`"), "id"), + Arguments.of("\"t1\".id", List.of("\"t1\""), "id"), + Arguments.of("[t1].id", List.of("[t1]"), "id"), + Arguments.of("`t1`.`id`", List.of("`t1`"), "`id`"), + Arguments.of("\"t1\".\"id\"", List.of("\"t1\""), "\"id\""), + Arguments.of("[t1].[id]", List.of("[t1]"), "[id]"), + Arguments.of("s.t1.`id`", List.of("t1", "s"), "`id`"), + Arguments.of("`s`.`t1`.`id`", List.of("`t1`", "`s`"), "`id`"), + Arguments.of("\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""), "\"id\""), + Arguments.of("[s].[t1].[id]", List.of("[t1]", "[s]"), "[id]"), + Arguments.of("[s].`t1`.\"id\"", List.of("`t1`", "[s]"), "\"id\""), + Arguments.of("t1.`i.d`", List.of("t1"), "`i.d`"), + Arguments.of("t1.`i``.d`", List.of("t1"), "`i``.d`"), + Arguments.of("t1.\"i\"\".d\"", List.of("t1"), "\"i\"\".d\""), + Arguments.of("t1.[i]].d]", List.of("t1"), "[i]].d]"), + Arguments.of("`t.1`.id", List.of("`t.1`"), "id"), + Arguments.of("`s.1`.`t.1`.`i.d`", List.of("`t.1`", "`s.1`"), "`i.d`"), + Arguments.of("\"s.1\".\"t.1\".\"i.d\"", List.of("\"t.1\"", "\"s.1\""), "\"i.d\""), + Arguments.of("[s.1].[t.1].[i.d]", List.of("[t.1]", "[s.1]"), "[i.d]"), + Arguments.of("`t``.1`.`i``.d`", List.of("`t``.1`"), "`i``.d`"), + Arguments.of("\"t\"\".1\".\"i\"\".d\"", List.of("\"t\"\".1\""), "\"i\"\".d\""), + Arguments.of("[t]].1].[i]].d]", List.of("[t]].1]"), "[i]].d]"), + Arguments.of("`t\".1`.\"i`.d\"", List.of("`t\".1`"), "\"i`.d\""), + Arguments.of("t1.id", List.of("t1"), "id"), + Arguments.of("s.t1.id", List.of("t1", "s"), "id")); + } + + static Stream separatedQuotedNames() { + // A single table token containing dots keeps the legacy BigQuery interpretation. + return qualifiedQuotedNames() + .filter(arguments -> { + List tableParts = (List) arguments.get()[1]; + return tableParts.size() > 1 || !((String) tableParts.get(0)).contains("."); + }); + } + + static Stream parsedQualifiedQuotedNames() { + return separatedQuotedNames() + .filter(arguments -> !((String) arguments.get()[0]).contains("]]")); + } + + static Stream legacyQualifiedTableNames() { + return Stream.of( + Arguments.of("schema.table", "schema.table.id", List.of("table", "schema")), + Arguments.of("`schema.table`", "\"schema\".\"table\".id", + List.of("\"table\"", "\"schema\"")), + Arguments.of("`project.dataset.table`", "\"project\".\"dataset\".\"table\".id", + List.of("\"table\"", "\"dataset\"", "\"project\""))); + } + + static Stream wholeQuotedNames() { + return Stream.of( + Arguments.of("`t1.id`", "\"t1\".\"id\"", List.of("\"t1\""), "\"id\""), + Arguments.of("\"t1.id\"", "\"t1\".\"id\"", List.of("\"t1\""), "\"id\""), + Arguments.of("[t1.id]", "\"t1\".\"id\"", List.of("\"t1\""), "\"id\""), + Arguments.of("`s.t1.id`", "\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""), + "\"id\""), + Arguments.of("\"s.t1.id\"", "\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""), + "\"id\""), + Arguments.of("[s.t1.id]", "\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""), + "\"id\"")); + } + } diff --git a/src/test/java/net/sf/jsqlparser/schema/MultiPartNameTest.java b/src/test/java/net/sf/jsqlparser/schema/MultiPartNameTest.java index dfb5034f61..113b019ac5 100644 --- a/src/test/java/net/sf/jsqlparser/schema/MultiPartNameTest.java +++ b/src/test/java/net/sf/jsqlparser/schema/MultiPartNameTest.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.schema; +import java.time.Duration; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.PlainSelect; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -21,4 +24,31 @@ void replaceBackticksWithDoubleQuotes() { Assertions.assertThat("\"starbake\".\"customers\"").isEqualToIgnoringCase( MultiPartName.replaceBackticksWithDoubleQuotes("`starbake`.`customers`")); } + + @Test + void unquoteMatchesQuotePattern() { + String[] names = {"", "a", "\"a\"", "[a]", "`a`", "\"a\".\"b\"", "\"\"\"\"", "\"]", + "[\"a\"]", "a\"", "\"a", "]a[", "``a``", "\"a\"\"b\"", "[[a]]"}; + for (String name : names) { + assertEquals(MultiPartName.LEADING_TRAILING_QUOTES_PATTERN.matcher(name).replaceAll(""), + MultiPartName.unquote(name), name); + assertEquals(MultiPartName.LEADING_TRAILING_QUOTES_PATTERN.matcher(name).find(), + MultiPartName.isQuoted(name), name); + } + assertNull(MultiPartName.unquote(null)); + assertFalse(MultiPartName.isQuoted(null)); + } + + @Test + void unquoteIsLinearOnRunsOfEscapedQuotes() { + // a quoted identifier with a dot and many doubled quotes used to backtrack quadratically + String quotes = "\"\"".repeat(40000); + String sql = "SELECT * FROM \"a" + quotes + ".b\""; + PlainSelect select = (PlainSelect) assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> CCJSqlParserUtil.parse(sql)); + assertNotNull(select.getFromItem()); + assertEquals("a" + quotes + ".b", + assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> MultiPartName.unquote("\"a" + quotes + ".b\""))); + } } diff --git a/src/test/java/net/sf/jsqlparser/schema/ServerTest.java b/src/test/java/net/sf/jsqlparser/schema/ServerTest.java index 216248be90..ed24ea52d4 100644 --- a/src/test/java/net/sf/jsqlparser/schema/ServerTest.java +++ b/src/test/java/net/sf/jsqlparser/schema/ServerTest.java @@ -10,10 +10,27 @@ package net.sf.jsqlparser.schema; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.time.Duration; import org.junit.jupiter.api.Test; public class ServerTest { + @Test + public void testCraftedServerNameDoesNotBacktrack() throws Exception { + final StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < 40000; i++) { + sb.append("a\\"); + } + final String crafted = sb.toString(); + + // an unterminated bracketed name full of backslashes used to backtrack quadratically + final Server server = assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> new Server(crafted)); + assertEquals(crafted, server.getFullyQualifiedName()); + } + @Test public void testServerNameParsing() throws Exception { final String serverName = "LOCALHOST"; diff --git a/src/test/java/net/sf/jsqlparser/schema/TableVariableTest.java b/src/test/java/net/sf/jsqlparser/schema/TableVariableTest.java new file mode 100644 index 0000000000..66d6a4a7de --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/schema/TableVariableTest.java @@ -0,0 +1,167 @@ +/*- + * #%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.schema; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.UserVariable; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TableVariableTest { + private Statement roundtrip(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql, p -> p.withUnsupportedStatements(false)); + assertThat(statement).isNotInstanceOf(UnsupportedStatement.class); + StringBuilder text = new StringBuilder(); + statement.accept(new StatementDeParser(text), null); + assertThat(text.toString()).isEqualTo(statement.toString()); + assertThat(CCJSqlParserUtil.parse(text.toString()).toString()) + .isEqualTo(statement.toString()); + return statement; + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT columnName FROM @table", "SELECT v.* FROM @table AS v", + "SELECT v.id FROM @table v JOIN dbo.source s ON v.id = s.id", + "SELECT * FROM (SELECT id FROM @table) t", + "WITH t AS (SELECT id FROM @table) SELECT * FROM t", + "INSERT INTO @table VALUES (1)", "INSERT INTO @table (id) SELECT id FROM source", + "UPDATE @table SET id = 2", "UPDATE t SET id = 3 FROM @table AS t", + "DELETE FROM @table", "DELETE @table WHERE id = 1", + "DELETE t FROM @table AS t", + "MERGE INTO @table AS t USING source AS s ON t.id = s.id " + + "WHEN MATCHED THEN UPDATE SET t.id = s.id"}) + void supportsTableVariablesInQueriesAndDml(String sql) throws JSQLParserException { + roundtrip(sql); + assertThat(Validation.validate(List.of(new FeaturesAllowed(Feature.values())), sql)) + .isEmpty(); + } + + @Test + void retainsTypedReferencesInExistingDmlModels() throws JSQLParserException { + Table from = (Table) ((PlainSelect) roundtrip("SELECT id FROM @t AS v")).getFromItem(); + Table inserted = ((Insert) roundtrip("INSERT INTO @t VALUES (1)")).getTable(); + Table updated = ((Update) roundtrip("UPDATE @t SET id = 2")).getTable(); + Table deleted = ((Delete) roundtrip("DELETE FROM @t")).getTable(); + for (Table table : List.of(from, inserted, updated, deleted)) { + assertThat(table.isTableVariable()).isTrue(); + assertThat(table.getName()).isEqualTo("@t"); + assertThat(table.getSchemaName()).isNull(); + assertThat(table.getASTNode()).isNotNull(); + Table copy = table.clone(); + assertThat(copy.isTableVariable()).isTrue(); + assertThat(copy.getName()).isEqualTo("@t"); + table.setUnsetCatalogAndSchema("catalog", "schema"); + assertThat(table.getFullyQualifiedName()).isEqualTo("@t"); + } + assertThat(from.getAlias().getName()).isEqualTo("v"); + } + + @Test + void separatesLocalSourcesFromCatalogTables() throws JSQLParserException { + String sql = "SELECT v.id FROM @t v JOIN dbo.source s ON v.id = s.id"; + assertThat(TablesNamesFinder.findTables(sql)).containsExactly("dbo.source"); + assertThat(TablesNamesFinder.findTablesOrOtherSources(sql)) + .containsExactlyInAnyOrder("@t", "dbo.source"); + assertThat(TablesNamesFinder.findTables("INSERT INTO @t SELECT id FROM source")) + .containsExactly("source"); + assertThat(TablesNamesFinder.findTables("UPDATE @t SET id = 2")).isEmpty(); + assertThat(TablesNamesFinder.findTables("DELETE FROM @t")).isEmpty(); + } + + @Test + void skipsOnlyLocalTableMetadataLookups() { + List checkedNames = new ArrayList<>(); + DatabaseMetaDataValidation metadata = named -> { + checkedNames.add(named.getFqn()); + return true; + }; + assertThat(Validation.validate(List.of(FeaturesAllowed.SELECT, metadata), + "SELECT 1 FROM @t CROSS JOIN dbo.source")).isEmpty(); + assertThat(checkedNames).containsExactly("dbo.source"); + checkedNames.clear(); + assertThat(Validation.validate(List.of(new FeaturesAllowed(Feature.values()), metadata), + "INSERT INTO @t VALUES (1)", "DELETE FROM @t")).isEmpty(); + assertThat(checkedNames).isEmpty(); + } + + @Test + void validatesTheDialectFeature() { + assertThat(Validation.validate(List.of(DatabaseType.SQLSERVER), "SELECT id FROM @t")) + .isEmpty(); + assertThat(Validation.validate(List.of(FeaturesAllowed.SELECT), "SELECT id FROM @t")) + .isEmpty(); + assertThat(Validation.validate(List.of(new FeaturesAllowed(Feature.select)), + "SELECT id FROM @t").toString()).contains("tableVariable not allowed"); + assertThat(Validation.validate(List.of(DatabaseType.POSTGRESQL), "SELECT id FROM @t") + .toString()).contains("tableVariable not supported"); + } + + @Test + void preservesScalarVariablesDeclarationsAndFollowingStatements() throws JSQLParserException { + PlainSelect select = (PlainSelect) roundtrip("SELECT @value FROM @t"); + assertThat(select.getSelectItem(0).getExpression()).isInstanceOf(UserVariable.class); + Statements statements = CCJSqlParserUtil.parseStatements( + "DECLARE @t TABLE (id INT); INSERT INTO @t VALUES (1); " + + "UPDATE @t SET id = 2; SELECT id FROM @t; DELETE FROM @t; SELECT 42;"); + assertThat(statements).hasSize(6); + assertThat(statements.get(5).toString()).isEqualTo("SELECT 42"); + assertThat(CCJSqlParserUtil.parseStatements(statements.toString())).hasSize(6); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * FROM dbo.t", "SELECT * FROM #temp", + "SELECT * FROM \"@catalog_table\"", "UPDATE t USE INDEX (idx) SET id = 1", + "CREATE INDEX idx ON t (id)", "SELECT id INTO copy FROM source"}) + void preservesCatalogTableGrammar(String sql) throws JSQLParserException { + Statement statement = roundtrip(sql); + if (statement instanceof PlainSelect) { + assertThat(((Table) ((PlainSelect) statement).getFromItem()).isTableVariable()) + .isFalse(); + } + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * FROM @@global", "SELECT * FROM dbo.@t", + "SELECT id INTO @t FROM source", + "CREATE INDEX idx ON @t (id)", "ALTER TABLE @t ADD value INT", + "DROP TABLE @t", "TRUNCATE TABLE @t", "SELECT @t.* FROM @t"}) + void rejectsCatalogOnlyAndMalformedReferences(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withUnsupportedStatements(false))); + } + + @Test + void doesNotAddCreateTableVariableSyntax() throws JSQLParserException { + // CREATE's existing fallback returns UnsupportedStatement even in strict mode. + assertThat(CCJSqlParserUtil.parse("CREATE TABLE @t (id INT)", + p -> p.withUnsupportedStatements(false))).isInstanceOf(UnsupportedStatement.class); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/AdapterRegressionTest.java b/src/test/java/net/sf/jsqlparser/statement/AdapterRegressionTest.java new file mode 100644 index 0000000000..3d6c93c3d3 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/AdapterRegressionTest.java @@ -0,0 +1,141 @@ +/*- + * #%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.statement; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Regressions for two adapter traversal defects. + */ +class AdapterRegressionTest { + + /** + * {@code SelectVisitorAdapter#visit(WithItem)} used to call {@code withItem.getSelect()}, an + * unchecked cast to {@code ParenthesedSelect}. Any data-modifying CTE reached through the + * select path threw {@link ClassCastException} - and the select path is taken for every + * sub-select, because {@code ExpressionVisitorAdapter#visit(Select)} loops the with-items into + * the select visitor. + */ + @Test + @DisplayName("a data-modifying CTE inside a sub-select no longer throws ClassCastException") + void dataModifyingCteOnTheSelectPath() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("SELECT * FROM u WHERE id IN (" + + "WITH c AS (DELETE FROM t RETURNING id) SELECT id FROM c)"); + + assertThatCode(() -> statement.accept(new StatementVisitorAdapter<>(), null)) + .doesNotThrowAnyException(); + } + + /** + * The CTE body must actually be traversed, not merely survived. + * + *

+ * This is the test that catches both defects, and it is the one that exposed the + * larger of the two. Reaching {@code visit(Delete)} here requires: + * + *

    + *
  1. the expression chain to be connected at all. {@code StatementVisitorAdapter()} used to + * build its {@code ExpressionVisitorAdapter} with the no-arg constructor, which leaves + * {@code selectVisitor} null - and {@code ExpressionVisitorAdapter#visit(Select)} returns + * immediately when it is. Every sub-select inside an expression (IN, EXISTS, scalar) was + * silently dropped, so no subclass could observe anything below one. Its + * {@code FromItemVisitorAdapter} had the mirror problem: the no-arg constructor builds a + * throwaway select/expression chain of its own.
  2. + *
  3. {@code SelectVisitorAdapter#visit(WithItem)} to dispatch the CTE body through + * {@code getParenthesedStatement()} rather than casting it to ParenthesedSelect, and to have a + * statement visitor to hand a DELETE body to.
  4. + *
+ * + *

+ * Defect (1) is also why (2) never surfaced as a ClassCastException in practice: the traversal + * never got that far. Fixing the wiring is what makes the cast reachable, so the two fixes have + * to land together. + * + *

+ * Note this can only be asserted through its effects - {@code StatementVisitorAdapter} exposes + * no getters for its sub-visitors, so the wiring cannot be inspected directly. Adding them + * would be a reasonable follow-up. + */ + @Test + @DisplayName("the CTE body is traversed, not skipped") + void dataModifyingCteBodyIsVisited() throws JSQLParserException { + List visited = new ArrayList<>(); + + StatementVisitorAdapter visitor = new StatementVisitorAdapter() { + @Override + public Void visit(net.sf.jsqlparser.statement.delete.Delete delete, S context) { + visited.add("delete"); + return super.visit(delete, context); + } + }; + + CCJSqlParserUtil + .parse("SELECT * FROM u WHERE id IN (" + + "WITH c AS (DELETE FROM t RETURNING id) SELECT id FROM c)") + .accept(visitor, null); + + assertThat(visited).containsExactly("delete"); + } + + /** + * A bare SelectVisitorAdapter has no statement visitor, so it cannot accept a DELETE body. It + * must skip it rather than fail - the old cast made this case fatal. + */ + @Test + @DisplayName("a standalone SelectVisitorAdapter skips a DML CTE instead of failing") + void standaloneSelectVisitorSkipsDmlCte() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("WITH c AS (DELETE FROM t RETURNING id) SELECT id FROM c"); + + SelectVisitorAdapter selects = new SelectVisitorAdapter<>(); + assertThat(selects.getStatementVisitor()).isNull(); + assertThatCode(() -> select.accept(selects, null)).doesNotThrowAnyException(); + } + + /** + * {@code StatementVisitorAdapter#visit(Delete)} never called {@code visitReturningClause}, + * unlike its Insert and Update counterparts, so expressions in the RETURNING list were never + * handed to the expression visitor. + */ + @Test + @DisplayName("DELETE .. RETURNING expressions are traversed") + void deleteReturningExpressionsAreVisited() throws JSQLParserException { + List functions = new ArrayList<>(); + + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Function function, S context) { + functions.add(function.getName().toLowerCase()); + return super.visit(function, context); + } + }; + + StatementVisitorAdapter visitor = + new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)); + + CCJSqlParserUtil.parse("DELETE FROM t WHERE a = 1 RETURNING upper(name)") + .accept(visitor, null); + + assertThat(functions).contains("upper"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/CascadeBehaviorTest.java b/src/test/java/net/sf/jsqlparser/statement/CascadeBehaviorTest.java new file mode 100644 index 0000000000..1f41e0d492 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/CascadeBehaviorTest.java @@ -0,0 +1,143 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.alter.AlterDomain; +import net.sf.jsqlparser.statement.alter.AlterType; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.truncate.Truncate; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class CascadeBehaviorTest { + static Stream statements() { + return Stream.of( + "ALTER DOMAIN amount DROP CONSTRAINT positive", + "ALTER TYPE item RENAME ATTRIBUTE old_name TO new_name", + "ALTER TYPE item ADD ATTRIBUTE code text", + "ALTER TYPE item DROP ATTRIBUTE price", + "ALTER TYPE item ALTER ATTRIBUTE price SET DATA TYPE bigint", + "DROP POLICY visible ON items", + "TRUNCATE TABLE items", + "REVOKE SELECT ON items FROM reader"); + } + + @ParameterizedTest + @MethodSource("statements") + void parsesSharedEnumAndPreservesOmittedClauses(String sql) throws JSQLParserException { + for (boolean postgres : new boolean[] {false, true}) { + for (CascadeBehavior behavior : new CascadeBehavior[] { + null, CascadeBehavior.CASCADE, CascadeBehavior.RESTRICT}) { + String expectedSql = sql + (behavior == null ? "" : " " + behavior); + Statement statement = parse(expectedSql, postgres); + assertSame(behavior, getBehavior(statement)); + assertRoundTrip(statement, expectedSql, behavior, postgres); + } + } + } + + @ParameterizedTest + @MethodSource("statements") + void sharedEnumSettersChangeAndRemoveClauses(String sql) throws JSQLParserException { + for (boolean postgres : new boolean[] {false, true}) { + Statement statement = parse(sql + " CASCADE", postgres); + for (CascadeBehavior behavior : new CascadeBehavior[] { + CascadeBehavior.RESTRICT, null, CascadeBehavior.CASCADE}) { + setBehavior(statement, behavior); + assertSame(behavior, getBehavior(statement)); + assertRoundTrip(statement, sql + (behavior == null ? "" : " " + behavior), + behavior, postgres); + } + } + } + + @Test + void truncateBooleanSetterStillClearsRatherThanSelectsRestrict() throws JSQLParserException { + Truncate statement = (Truncate) parse("TRUNCATE TABLE items RESTRICT", true); + assertFalse(statement.getCascade()); + statement.setCascade(true); + assertTrue(statement.getCascade()); + assertSame(CascadeBehavior.CASCADE, statement.getDropBehavior()); + assertRoundTrip(statement, "TRUNCATE TABLE items CASCADE", CascadeBehavior.CASCADE, true); + + statement.setCascade(false); + assertFalse(statement.getCascade()); + assertNull(statement.getDropBehavior()); + assertRoundTrip(statement, "TRUNCATE TABLE items", null, true); + } + + private static Statement parse(String sql, boolean postgres) throws JSQLParserException { + return postgres + ? CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)) + : CCJSqlParserUtil.parse(sql); + } + + private static void assertRoundTrip(Statement statement, String expectedSql, + CascadeBehavior behavior, boolean postgres) throws JSQLParserException { + assertEquals(expectedSql, statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(expectedSql, deparsed.toString()); + Statement reparsed = parse(deparsed.toString(), postgres); + assertSame(behavior, getBehavior(reparsed)); + assertEquals(expectedSql, reparsed.toString()); + } + + private static CascadeBehavior getBehavior(Statement statement) { + if (statement instanceof AlterDomain) { + return ((AlterDomain) statement).getBehavior(); + } else if (statement instanceof AlterType) { + AlterType type = (AlterType) statement; + return type.getAttributeChanges().isEmpty() ? type.getBehavior() + : type.getAttributeChanges().get(0).getBehavior(); + } else if (statement instanceof DropPolicy) { + return ((DropPolicy) statement).getBehavior(); + } else if (statement instanceof Truncate) { + return ((Truncate) statement).getDropBehavior(); + } else if (statement instanceof Revoke) { + return ((Revoke) statement).getBehavior(); + } + throw new IllegalArgumentException("Unsupported statement: " + statement); + } + + private static void setBehavior(Statement statement, CascadeBehavior behavior) { + if (statement instanceof AlterDomain) { + ((AlterDomain) statement).setBehavior(behavior); + } else if (statement instanceof AlterType) { + AlterType type = (AlterType) statement; + if (type.getAttributeChanges().isEmpty()) { + type.setBehavior(behavior); + } else { + type.getAttributeChanges().get(0).setBehavior(behavior); + } + } else if (statement instanceof DropPolicy) { + ((DropPolicy) statement).setBehavior(behavior); + } else if (statement instanceof Truncate) { + ((Truncate) statement).setDropBehavior(behavior); + } else if (statement instanceof Revoke) { + ((Revoke) statement).setBehavior(behavior); + } else { + throw new IllegalArgumentException("Unsupported statement: " + statement); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/ConfigurationParameterChangeTest.java b/src/test/java/net/sf/jsqlparser/statement/ConfigurationParameterChangeTest.java new file mode 100644 index 0000000000..4f70e77464 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/ConfigurationParameterChangeTest.java @@ -0,0 +1,166 @@ +/*- + * #%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.statement; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.ConfigurationParameterChange.Action; +import net.sf.jsqlparser.statement.ConfigurationParameterChange.ValueKind; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ConfigurationParameterChangeTest { + + @ParameterizedTest + @ValueSource(strings = {"SET search_path TO app, public", "SET work_mem = '64MB'", + "SET work_mem TO DEFAULT", "SET work_mem = DEFAULT", "SET work_mem FROM CURRENT", + "SET myext.setting = 1", "RESET work_mem", "RESET ALL"}) + void testAlterRoleAndDatabaseShareTheClause(String clause) throws JSQLParserException { + AlterRole role = (AlterRole) CCJSqlParserUtil.parse("ALTER ROLE app " + clause); + AlterDatabase database = + (AlterDatabase) CCJSqlParserUtil.parse("ALTER DATABASE app " + clause); + + assertThat(role.getParameterChange().toString()).isEqualTo(clause); + assertThat(database.getParameterChange().toString()).isEqualTo(clause); + assertThat(database.getAction()) + .isEqualTo(AlterDatabase.Action.CONFIGURATION_PARAMETER); + assertThat(role.getAction().name()) + .isEqualTo(role.getParameterChange().getAction().name()); + assertThat(deparse(role)).isEqualTo("ALTER ROLE app " + clause); + assertThat(deparse(database)).isEqualTo("ALTER DATABASE app " + clause); + } + + @Test + void testParsedState() throws JSQLParserException { + ConfigurationParameterChange set = parseChange("SET search_path TO app, public"); + assertThat(set.getAction()).isEqualTo(Action.SET); + assertThat(set.getParameter()).isEqualTo("search_path"); + assertThat(set.getValueKind()).isEqualTo(ValueKind.VALUES); + assertThat(set.isUseEquals()).isFalse(); + assertThat(set.getValues()).extracting(Object::toString).containsExactly("app", "public"); + + ConfigurationParameterChange setDefault = parseChange("SET work_mem = DEFAULT"); + assertThat(setDefault.getValueKind()).isEqualTo(ValueKind.DEFAULT); + assertThat(setDefault.isUseEquals()).isTrue(); + assertThat(setDefault.getValues()).isNull(); + + assertThat(parseChange("SET work_mem FROM CURRENT").getValueKind()) + .isEqualTo(ValueKind.FROM_CURRENT); + + ConfigurationParameterChange resetAll = parseChange("RESET ALL"); + assertThat(resetAll.getAction()).isEqualTo(Action.RESET); + assertThat(resetAll.getParameter()).isEqualTo("ALL"); + } + + @Test + void testBuildAndEdit() { + ConfigurationParameterChange change = + new ConfigurationParameterChange(Action.SET, "statement_timeout") + .withUseEquals(true) + .withValues(new ExpressionList(new StringValue("5s"))); + assertThat(change.toString()).isEqualTo("SET statement_timeout = '5s'"); + + change.setValueKind(ValueKind.DEFAULT); + assertThat(change.toString()).isEqualTo("SET statement_timeout = DEFAULT"); + change.setValueKind(ValueKind.VALUES); + assertThat(change.toString()).isEqualTo("SET statement_timeout = '5s'"); + change.setValueKind(ValueKind.FROM_CURRENT); + assertThat(change.toString()).isEqualTo("SET statement_timeout FROM CURRENT"); + + change.setAction(Action.RESET); + assertThat(change.toString()).isEqualTo("RESET statement_timeout"); + + AlterDatabase database = new AlterDatabase(); + database.setDatabaseName("app"); + database.setAction(AlterDatabase.Action.CONFIGURATION_PARAMETER); + database.setParameterChange(change); + assertThat(database.toString()).isEqualTo("ALTER DATABASE app RESET statement_timeout"); + } + + @Test + void testAlterRoleLegacyAccessorsReadTheChange() throws JSQLParserException { + AlterRole role = (AlterRole) CCJSqlParserUtil + .parse("ALTER ROLE ALL IN DATABASE appdb SET search_path = app, public"); + assertThat(role.getAction()).isEqualTo(AlterRole.Action.SET); + assertThat(role.getParameter()).isEqualTo("search_path"); + assertThat(role.isUseEquals()).isTrue(); + assertThat(role.isUseDefault()).isFalse(); + assertThat(role.isFromCurrent()).isFalse(); + assertThat(role.getValues()).isSameAs(role.getParameterChange().getValues()); + + role.getValues().set(0, new Column("new_schema")); + role.setUseEquals(false); + assertThat(role.toString()) + .isEqualTo( + "ALTER ROLE ALL IN DATABASE appdb SET search_path TO new_schema, public"); + + role.setUseDefault(true); + assertThat(role.getParameterChange().getValueKind()).isEqualTo(ValueKind.DEFAULT); + assertThat(role.toString()).endsWith("SET search_path TO DEFAULT"); + role.setFromCurrent(true); + assertThat(role.isUseDefault()).isFalse(); + assertThat(role.toString()).endsWith("SET search_path FROM CURRENT"); + role.setFromCurrent(false); + assertThat(role.toString()).endsWith("SET search_path TO new_schema, public"); + + role.setAction(AlterRole.Action.RESET); + assertThat(role.getParameterChange().getAction()).isEqualTo(Action.RESET); + assertThat(role.toString()).isEqualTo("ALTER ROLE ALL IN DATABASE appdb RESET search_path"); + + role.getParameterChange().setAction(Action.SET); + assertThat(role.getAction()).isEqualTo(AlterRole.Action.SET); + } + + @Test + void testAlterRoleLegacySetters() { + AlterRole role = new AlterRole(); + role.setName("app"); + assertThat(role.getParameterChange()).isNull(); + assertThat(role.getParameter()).isNull(); + assertThat(role.getValues()).isNull(); + assertThat(role.isUseEquals()).isFalse(); + + role.setParameter("work_mem"); + role.setAction(AlterRole.Action.SET); + role.setValues(new ExpressionList(new StringValue("64MB"))); + assertThat(role.toString()).isEqualTo("ALTER ROLE app SET work_mem TO '64MB'"); + + role.setAction(AlterRole.Action.RENAME); + role.setNewName("app2"); + assertThat(role.toString()).isEqualTo("ALTER ROLE app RENAME TO app2"); + assertThat(role.getParameter()).isEqualTo("work_mem"); + + role.setParameterChange(new ConfigurationParameterChange(Action.RESET, "ALL")); + assertThat(role.getAction()).isEqualTo(AlterRole.Action.RESET); + assertThat(role.toString()).isEqualTo("ALTER ROLE app RESET ALL"); + } + + private static ConfigurationParameterChange parseChange(String clause) + throws JSQLParserException { + return ((AlterDatabase) CCJSqlParserUtil.parse("ALTER DATABASE d " + clause)) + .getParameterChange(); + } + + private static String deparse(Statement statement) { + StringBuilder builder = new StringBuilder(); + statement.accept(new StatementDeParser(builder)); + return builder.toString(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/DdlVisitorTraversalTest.java b/src/test/java/net/sf/jsqlparser/statement/DdlVisitorTraversalTest.java new file mode 100644 index 0000000000..0efd5168b1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/DdlVisitorTraversalTest.java @@ -0,0 +1,156 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class DdlVisitorTraversalTest { + static Stream definitions() { + return Stream.of( + Arguments.of("ALTER VIEW v ALTER COLUMN id SET DEFAULT 7", List.of("7"), + List.of("v")), + Arguments.of("ALTER INDEX ix SET (fillfactor = 70)", List.of("70"), List.of()), + Arguments.of("ALTER POLICY p ON t USING (id > 7) WITH CHECK (id < 9)", + List.of("7", "9"), List.of("t")), + Arguments.of("DROP POLICY p ON t", List.of(), List.of("t")), + Arguments.of("CREATE STATISTICS s ON (id + 7), label FROM t", List.of("7"), + List.of("t")), + Arguments.of("CREATE FOREIGN DATA WRAPPER w OPTIONS (host 'before')", + List.of("before"), List.of()), + Arguments.of("ALTER FOREIGN DATA WRAPPER w OPTIONS (SET host 'before')", + List.of("before"), List.of()), + Arguments.of( + "CREATE SERVER s TYPE 'pg' VERSION '18' FOREIGN DATA WRAPPER w OPTIONS (host 'before')", + List.of("before", "pg", "18"), List.of()), + Arguments.of("ALTER SERVER s VERSION '18' OPTIONS (SET host 'before')", + List.of("before", "18"), List.of()), + Arguments.of( + "CREATE USER MAPPING FOR CURRENT_USER SERVER s OPTIONS (user 'before')", + List.of("before"), List.of()), + Arguments.of( + "ALTER USER MAPPING FOR CURRENT_USER SERVER s OPTIONS (SET user 'before')", + List.of("before"), List.of()), + Arguments.of("CREATE COLLATION c (locale = 'C')", List.of("C"), List.of()), + Arguments.of( + "CREATE RULE r AS ON INSERT TO t WHERE NEW.id > 7 DO (INSERT INTO log VALUES (9); NOTIFY ch, 'before')", + List.of("7", "9", "before"), List.of("t", "log"))); + } + + @Test + void valuesChildrenIncludingWithAndTailKeepTheVisitorContext() throws Exception { + Object context = new Object(); + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S actualContext) { + assertSame(context, actualContext); + values.add(value.getValue()); + return null; + } + }; + Statement statement = CCJSqlParserUtil.parse( + "WITH c AS (SELECT 11) VALUES (22) ORDER BY (33 + 0) LIMIT 44 OFFSET 55"); + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + context); + assertEquals(List.of(11L, 22L, 33L, 0L, 44L, 55L), values); + } + + @ParameterizedTest + @MethodSource("definitions") + void visitsAndMutatesChildrenWithContext(String sql, List expectedValues, + List expectedTables) throws Exception { + Statement statement = CCJSqlParserUtil.parse(sql); + List values = new ArrayList<>(); + List tables = new ArrayList<>(); + Object context = new Object(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S actualContext) { + assertSame(context, actualContext); + values.add(value.getStringValue()); + value.setValue(value.getValue() + 1); + return null; + } + + @Override + public Void visit(StringValue value, S actualContext) { + assertSame(context, actualContext); + values.add(value.getValue()); + value.setValue("changed"); + return null; + } + }; + FromItemVisitorAdapter fromItems = new FromItemVisitorAdapter() { + @Override + public Void visit(Table table, S actualContext) { + assertSame(context, actualContext); + tables.add(table.getName()); + table.setName("changed_" + table.getName()); + return null; + } + }; + SelectVisitorAdapter selects = + new SelectVisitorAdapter<>(expressions, fromItems); + statement.accept(new StatementVisitorAdapter<>(selects), context); + assertEquals(expectedValues, values); + assertEquals(expectedTables, tables); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER VIEW v ALTER COLUMN id SET DEFAULT abs(7)", + "ALTER POLICY p ON t USING (abs(id) > 7)", + "CREATE STATISTICS s ON (abs(id)) FROM t"}) + void validatesNestedExpressions(String sql) { + FeaturesAllowed withoutFunctions = new FeaturesAllowed().add(FeaturesAllowed.DDL); + assertFalse(new Validation(List.of(withoutFunctions), sql).validate().isEmpty()); + FeaturesAllowed withFunctions = + new FeaturesAllowed().add(FeaturesAllowed.DDL).add(Feature.function); + assertTrue(new Validation(List.of(withFunctions), sql).validate().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE RULE r AS ON INSERT TO t DO INSERT INTO log VALUES (7)", + "CREATE RULE r AS ON SELECT TO v DO INSTEAD SELECT * FROM t", + "ALTER POLICY p ON t USING (id > 7)", + "CREATE STATISTICS s ON (id + 7), label FROM t"}) + void storedDefinitionsAreSchemaChangesRatherThanExecutedBodies(String sql) throws Exception { + StatementFeatures features = CCJSqlParserUtil.parse(sql).getFeatures(); + assertTrue(features.is(StmtFeature.MODIFIES_SCHEMA)); + assertFalse(features.is(StmtFeature.MODIFIES_DATA)); + assertFalse(features.is(StmtFeature.RETURNS_RESULT_SET)); + assertFalse(features.is(StmtFeature.READS_DATA)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/DeclareStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/DeclareStatementTest.java index c8b5ca1ac6..7ec516f7f5 100644 --- a/src/test/java/net/sf/jsqlparser/statement/DeclareStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/DeclareStatementTest.java @@ -16,6 +16,7 @@ import net.sf.jsqlparser.statement.DeclareStatement.TypeDefExpr; import net.sf.jsqlparser.statement.create.table.ColDataType; import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; import static net.sf.jsqlparser.test.TestUtils.asList; import static net.sf.jsqlparser.test.TestUtils.assertDeparse; import static net.sf.jsqlparser.test.TestUtils.assertEqualsObjectTree; @@ -37,7 +38,8 @@ public void testDeclareType() throws JSQLParserException { DeclareStatement created = new DeclareStatement() .addTypeDefExprList( new TypeDefExpr(new UserVariable().withName("find"), - new ColDataType().withDataType("nvarchar (30)"), null)) + new ColDataType().withDataType("nvarchar (30)").withPrecision(30), + null)) .withDeclareType(DeclareType.TYPE); assertDeparse(created, statement); assertEqualsObjectTree(parsed, created); @@ -49,7 +51,7 @@ public void testDeclareTypeWithDefault() throws JSQLParserException { Statement parsed = assertSqlCanBeParsedAndDeparsed(statement); DeclareStatement created = new DeclareStatement() .addTypeDefExprList(new TypeDefExpr(new UserVariable().withName("find"), - new ColDataType().withDataType("varchar (30)"), + new ColDataType().withDataType("varchar (30)").withPrecision(30), new StringValue().withValue("Man%"))) .withDeclareType(DeclareType.TYPE); assertDeparse(created, statement); @@ -63,7 +65,7 @@ public void testDeclareTypeList() throws JSQLParserException { DeclareStatement created = new DeclareStatement().addTypeDefExprList(asList( // new TypeDefExpr( new UserVariable().withName("group"), - new ColDataType().withDataType("nvarchar (50)"), + new ColDataType().withDataType("nvarchar (50)").withPrecision(50), null), new TypeDefExpr(new UserVariable().withName("sales"), new ColDataType().withDataType("money"), null))) @@ -86,8 +88,8 @@ public void testDeclareTable() throws JSQLParserException { .withUserVariable(new UserVariable("MyTableVar")) .withColumnDefinitions(new ArrayList<>()) .addColumnDefinitions( - new ColumnDefinition("EmpID", new ColDataType().withDataType("int"), - asList("NOT", "NULL")), + new ColumnDefinition("EmpID", new ColDataType().withDataType("int")) + .addColumnOptions(ColumnOption.nullability(false)), new ColumnDefinition("OldVacationHours", new ColDataType("int"))) .addColumnDefinitions( asList( diff --git a/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java b/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java new file mode 100644 index 0000000000..6a4f7b40c0 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java @@ -0,0 +1,99 @@ +/*- + * #%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.statement; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.PivotVisitorAdapter; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.statement.select.SelectItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DistinctVisitorTest { + @ParameterizedTest + @ValueSource(strings = {"SELECT DISTINCT id FROM foo", + "SELECT * FROM (SELECT DISTINCT id FROM foo) x", + "WITH x AS (SELECT DISTINCT id FROM foo) SELECT * FROM x", + "SELECT DISTINCT ON (id) id FROM foo", "SELECT ALL id FROM foo"}) + void analysisPreservesSelectSyntax(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + String before = statement.toString(); + StatementFeatures features = statement.getFeatures(); + assertThat(features.returnsResultSet()).isTrue(); + assertThat(features.mayModifyData()).isFalse(); + assertThat(features.getUnresolvedReferences()).isEmpty(); + statement.accept(new StatementVisitorAdapter<>(), null); + assertThat(statement.toString()).isEqualTo(before); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed), null); + assertThat(CCJSqlParserUtil.parse(deparsed.toString()).toString()).isEqualTo(before); + } + + @Test + void plainDistinctKeepsItsAbsentOnList() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT DISTINCT id FROM foo"); + select.accept(new SelectVisitorAdapter<>(), null); + assertThat(select.getDistinct().getOnSelectItems()).isNull(); + assertThat(select.toString()).isEqualTo("SELECT DISTINCT id FROM foo"); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT DISTINCT unproven(id) FROM foo", + "SELECT * FROM (SELECT DISTINCT unproven(id) FROM foo) x", + "WITH x AS (SELECT DISTINCT unproven(id) FROM foo) SELECT * FROM x"}) + void plainDistinctStillReportsUnprovenFunctions(String sql) throws JSQLParserException { + StatementFeatures features = CCJSqlParserUtil.parse(sql).getFeatures(n -> false); + assertThat(features.getUnresolvedReferences()).containsExactly("unproven"); + assertThat(features.mayModifyData()).isTrue(); + assertThat(features.modifiesData()).isFalse(); + } + + @Test + void selectAndOutputItemsKeepCallbackOrderAndContext() throws JSQLParserException { + List seen = new ArrayList<>(); + Object marker = new Object(); + SelectItemVisitorAdapter items = new SelectItemVisitorAdapter() { + @Override + public Void visit(SelectItem item, S context) { + assertThat(context).isSameAs(marker); + seen.add(item.getExpression().toString()); + return null; + } + }; + SelectVisitorAdapter visitor = new SelectVisitorAdapter<>( + new ExpressionVisitorAdapter<>(), new PivotVisitorAdapter<>(), items, + new FromItemVisitorAdapter<>()); + Select select = (Select) CCJSqlParserUtil + .parse("SELECT DISTINCT ON (key_fn(id)) value_fn(id), id FROM foo"); + select.accept(visitor, marker); + assertThat(seen).containsExactly("key_fn(id)", "value_fn(id)", "id"); + assertThat(select.getFeatures().getUnresolvedReferences()) + .containsExactly("key_fn", "value_fn"); + + seen.clear(); + PlainSelect output = (PlainSelect) CCJSqlParserUtil.parse("SELECT a, b"); + visitor.visitOutputClause(new OutputClause(output.getSelectItems(), null, null, null), + marker); + assertThat(seen).containsExactly("a", "b"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/DoStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/DoStatementTest.java new file mode 100644 index 0000000000..6c5b495ec4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/DoStatementTest.java @@ -0,0 +1,150 @@ +/*- + * #%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.statement; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DoStatementTest { + @ParameterizedTest + @ValueSource(strings = {"DO $$BEGIN NULL; END$$", "DO LANGUAGE plpgsql $$BEGIN NULL; END$$", + "DO $body$BEGIN NULL; END$body$ LANGUAGE plpgsql", "DO 'BEGIN NULL; END'", + "DO LANGUAGE plpython3u 'print(1)'"}) + void roundTripsBodyAndLanguagePosition(String sql) throws Exception { + DoStatement statement = (DoStatement) assertSqlCanBeParsedAndDeparsed(sql, false, + p -> p.withDialect(Dialect.POSTGRESQL)); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), + p -> p.withDialect(Dialect.POSTGRESQL)).toString()); + assertEquals(sql.startsWith("DO LANGUAGE"), statement.isLanguageBeforeCode()); + assertEquals( + sql.contains("LANGUAGE") ? sql.contains("plpython") ? "plpython3u" : "plpgsql" + : null, + statement.getLanguage()); + assertTrue(statement.getFeatures().isOpaque()); + assertTrue(statement.getFeatures().mayModifyData()); + assertThrows(UnsupportedOperationException.class, + () -> new TablesNamesFinder().getTables(statement)); + } + + @Test + void preservesProceduralBodyAndFollowingStatementsIssue1946() throws Exception { + String sql; + try (InputStream input = getClass().getResourceAsStream("/postgresql/do-issue1946.sql")) { + assertNotNull(input); + sql = new String(input.readAllBytes(), StandardCharsets.UTF_8).strip(); + } + String body = sql.substring(sql.indexOf("$$"), sql.lastIndexOf("$$") + 2); + Statements statements = + CCJSqlParserUtil.parseStatements("SELECT 0;\n" + sql + "\nSELECT 1;", + p -> p.withDialect(Dialect.POSTGRESQL).withUnsupportedStatements(false)); + assertEquals(3, statements.size()); + assertEquals("SELECT 0", statements.get(0).toString()); + DoStatement block = assertInstanceOf(DoStatement.class, statements.get(1)); + assertEquals(body, block.getCode().toString()); + assertEquals(body.substring(2, body.length() - 2), block.getCode().getValue()); + assertEquals("SELECT 1", statements.get(2).toString()); + + StringBuilder output = new StringBuilder(); + for (Statement statement : statements) { + statement.accept(new StatementDeParser(output), null); + output.append(";\n"); + } + assertEquals("SELECT 0;\nDO " + body + ";\nSELECT 1;\n", output.toString()); + Statements reparsed = CCJSqlParserUtil.parseStatements(output.toString(), + p -> p.withDialect(Dialect.POSTGRESQL).withUnsupportedStatements(false)); + assertEquals(3, reparsed.size()); + assertEquals(body, + assertInstanceOf(DoStatement.class, reparsed.get(1)).getCode().toString()); + assertEquals("SELECT 0", reparsed.get(0).toString()); + assertEquals("SELECT 1", reparsed.get(2).toString()); + } + + @Test + void supportsBodyVisitorsAndAstEdits() throws Exception { + DoStatement statement = (DoStatement) CCJSqlParserUtil.parse("DO $$BEGIN NULL; END$$", + p -> p.withDialect(Dialect.POSTGRESQL)); + List bodies = new ArrayList<>(); + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>( + new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + assertEquals("context", context); + bodies.add(value.toString()); + return null; + } + })), "context"); + assertEquals(List.of("$$BEGIN NULL; END$$"), bodies); + statement.withCode(new StringValue("$new$BEGIN PERFORM 1; END$new$")) + .withLanguage("plpgsql"); + statement.setLanguageBeforeCode(true); + assertEquals("DO LANGUAGE plpgsql $new$BEGIN PERFORM 1; END$new$", statement.toString()); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + return getBuilder().append("$$BEGIN NULL; END$$"); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output)); + assertEquals("DO LANGUAGE plpgsql $$BEGIN NULL; END$$", output.toString()); + } + + @Test + void gatesDialectAndRejectsMalformedWrappers() throws Exception { + String sql = "DO $$BEGIN NULL; END$$"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.POSTGRESQL) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect))); + } + } + for (String malformed : List.of("DO", "DO LANGUAGE plpgsql", "DO 123", + "DO LANGUAGE plpgsql $$x$$ LANGUAGE plpgsql", "DO $tag$unterminated")) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(malformed, + p -> p.withDialect(Dialect.POSTGRESQL))); + } + } + + @Test + void validatesWrapperCapabilityWithoutClaimingBodyValidation() { + FeatureConfiguration config = new FeatureConfiguration() + .setValue(Feature.dialect, Dialect.POSTGRESQL.name()); + assertTrue(new Validation(config, List.of(PostgresqlVersion.V10), + "DO $$arbitrary language body$$") + .validate().isEmpty()); + assertFalse(new Validation(config, List.of(new FeaturesAllowed(Feature.select)), + "DO $$arbitrary language body$$").validate().isEmpty()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/ForPortionClauseTest.java b/src/test/java/net/sf/jsqlparser/statement/ForPortionClauseTest.java new file mode 100644 index 0000000000..012ac9df97 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/ForPortionClauseTest.java @@ -0,0 +1,171 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.MariaDbVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ForPortionClauseTest extends ValidationTestAsserts { + @ParameterizedTest + @ValueSource(strings = { + "UPDATE prices FOR PORTION OF VALID_TIME FROM '2020-01-01' TO '2021-01-01' SET price = 10 WHERE id = 1", + "DELETE FROM prices FOR PORTION OF VALID_TIME FROM '2020-01-01' TO '2021-01-01' WHERE id = 1", + "UPDATE prices p FOR PORTION OF valid_time FROM ? TO ? SET price = ? WHERE id = ?", + "DELETE FROM prices AS p FOR PORTION OF \"valid time\" FROM :start_date TO :end_date WHERE id = :id", + "UPDATE prices FOR PORTION OF p FROM DATE '2020-01-01' TO DATE '2021-01-01' SET price = 10", + "DELETE FROM prices FOR PORTION OF p FROM CAST(? AS DATE) TO CAST(? AS DATE) ORDER BY id LIMIT 1", + "UPDATE prices FOR PORTION OF p FROM (1 + 2) TO COALESCE(3, 4) SET price = 10 RETURNING id", + "DELETE FROM prices FOR PORTION OF p FROM 1 TO 2 RETURNING id", + "WITH ids AS (SELECT id FROM selected) UPDATE prices FOR PORTION OF p FROM 1 TO 2 SET price = 10 WHERE id IN (SELECT id FROM ids)"}) + void preservesTheIntervalThroughBothStatementRenderers(String sql) throws Exception { + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + ForPortionClause clause = portion(statement); + assertEquals(clause.toString(), + portion(CCJSqlParserUtil.parse(statement.toString())).toString()); + } + + @Test + void exposesTypedBoundsAndPreservesGlobalParameterOrder() throws Exception { + Update update = (Update) CCJSqlParserUtil.parse( + "UPDATE prices FOR PORTION OF p FROM ? TO ? SET price = ? WHERE id = ?"); + ForPortionClause clause = update.getForPortionClause(); + assertEquals("p", clause.getPeriodName()); + assertEquals(1, ((JdbcParameter) clause.getFromExpression()).getIndex()); + assertEquals(2, ((JdbcParameter) clause.getToExpression()).getIndex()); + assertEquals(3, + ((JdbcParameter) update.getUpdateSets().get(0).getValues().get(0)).getIndex()); + clause.setPeriodName("valid_time"); + clause.setFromExpression(new StringValue("2020-01-01")); + clause.setToExpression(new StringValue("2021-01-01")); + assertInstanceOf(StringValue.class, clause.getFromExpression()); + assertEquals( + "UPDATE prices FOR PORTION OF valid_time FROM '2020-01-01' TO '2021-01-01' SET price = ? WHERE id = ?", + update.toString()); + Delete delete = (Delete) CCJSqlParserUtil.parse("DELETE FROM prices"); + assertNull(delete.getForPortionClause()); + assertSame(delete, delete.withForPortionClause(clause)); + assertEquals("DELETE FROM prices " + clause, delete.toString()); + update.setForPortionClause(null); + assertEquals("UPDATE prices SET price = ? WHERE id = ?", update.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"UPDATE prices FOR PORTION OF p FROM 1 TO 2 SET price = 3 WHERE id = 4", + "DELETE FROM prices FOR PORTION OF p FROM 1 TO 2 WHERE id = 4"}) + void customVisitorsSeeEachBoundExactlyOnce(String sql) throws Exception { + Statement statement = CCJSqlParserUtil.parse(sql); + Object context = new Object(); + List bounds = new ArrayList<>(); + portion(statement).accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S actualContext) { + assertSame(context, actualContext); + bounds.add(value.getValue()); + return null; + } + }, context); + assertEquals(List.of(1L, 2L), bounds); + List rendered = new ArrayList<>(); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S actualContext) { + rendered.add(value.getValue()); + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(statement instanceof Update ? List.of(1L, 2L, 3L, 4L) : List.of(1L, 2L, 4L), + rendered); + String expected = sql.replace("FROM 1 TO 2", "FROM 101 TO 102") + .replace("price = 3", "price = 103").replace("id = 4", "id = 104"); + assertEquals(expected, output.toString()); + assertEquals(sql, statement.toString()); + TestUtils.assertSqlCanBeParsedAndDeparsed(output.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE prices FOR PORTION OF p FROM (SELECT MIN(ts) FROM starts) TO (SELECT MAX(ts) FROM ends) SET price = 10", + "DELETE FROM prices FOR PORTION OF p FROM (SELECT MIN(ts) FROM starts) TO (SELECT MAX(ts) FROM ends)"}) + void tableDiscoveryTraversesBothBoundExpressions(String sql) throws Exception { + assertEquals(Set.of("prices", "starts", "ends"), TablesNamesFinder.findTables(sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"UPDATE prices FOR PORTION OF p FROM ? TO ? SET price = 10", + "DELETE FROM prices FOR PORTION OF p FROM ? TO ?", + "UPDATE prices FOR PORTION OF p FROM ? TO 2 SET price = 10", + "UPDATE prices FOR PORTION OF p FROM 1 TO ? SET price = 10", + "DELETE FROM prices FOR PORTION OF p FROM ? TO 2", + "DELETE FROM prices FOR PORTION OF p FROM 1 TO ?"}) + void validatesTheClauseFeatureAndBothBoundExpressions(String sql) throws Exception { + validateNoErrors(sql, 1, + new FeaturesAllowed().add(FeaturesAllowed.DML).add(Feature.jdbcParameter)); + validateNoErrors(sql, 1, MariaDbVersion.V10_5_4); + validateNotAllowed(sql, 1, 1, + new FeaturesAllowed().add(FeaturesAllowed.DML).add(Feature.jdbcParameter) + .remove(Feature.forPortion), + Feature.forPortion); + validateNotAllowed(sql, 1, 1, + new FeaturesAllowed().add(FeaturesAllowed.DML).remove(Feature.jdbcParameter), + Feature.jdbcParameter); + } + + @ParameterizedTest + @ValueSource(strings = {"UPDATE prices FOR PORTION p FROM 1 TO 2 SET price = 10", + "DELETE FROM prices FOR PORTION OF p FROM 1", + "DELETE FROM prices FOR PORTION OF p FROM 1 TO", + "DELETE FROM prices FOR PORTION OF p TO 2", + "UPDATE prices FOR PORTION OF p FROM 1 TO 2", + "DELETE FROM prices FOR PORTION OF p FROM 1 TO 2 garbage"}) + void rejectsIncompleteClausesAndTrailingInput(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"UPDATE portion SET valid_time = 1", + "DELETE FROM portion WHERE valid_time = 1", + "SELECT portion, valid_time FROM prices FOR UPDATE"}) + void preservesOrdinaryIdentifiersAndForUpdate(String sql) throws Exception { + TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + } + + private static ForPortionClause portion(Statement statement) { + return statement instanceof Update ? ((Update) statement).getForPortionClause() + : ((Delete) statement).getForPortionClause(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java index b595ab096f..d07d818aab 100644 --- a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java @@ -9,15 +9,54 @@ */ package net.sf.jsqlparser.statement; +import java.util.function.Consumer; +import java.util.stream.Stream; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.AnalyticExpression; +import net.sf.jsqlparser.expression.ConnectByPriorOperator; +import net.sf.jsqlparser.expression.JsonAggregateOnNullType; +import net.sf.jsqlparser.expression.JsonFunction; +import net.sf.jsqlparser.expression.TernaryExpression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.WindowElement; +import net.sf.jsqlparser.expression.WindowOffset; +import net.sf.jsqlparser.expression.XMLSerializeExpr; +import net.sf.jsqlparser.statement.piped.FromQuery; +import net.sf.jsqlparser.statement.piped.SelectPipeOperator; +import net.sf.jsqlparser.expression.NotExpression; +import net.sf.jsqlparser.expression.MapExpression; +import net.sf.jsqlparser.expression.JdbcNamedParameter; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.IsUnknownExpression; +import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.parser.ParserKeywordsUtils; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.Join; +import net.sf.jsqlparser.statement.select.MySqlSelectModifier; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.SampleClause; +import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.util.TablesNamesFinder; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; - -import java.util.stream.Stream; +import org.junit.jupiter.params.provider.ValueSource; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; /** * Verifies that all non-reserved keywords can be used as unquoted identifiers (schema, table, @@ -51,4 +90,468 @@ public void testCombinedTokenKeywords() throws JSQLParserException { String sqlStr = "SELECT current_date(3)"; assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + // #2473: GROUPS is a non-reserved keyword in PostgreSQL and must stay usable as an + // identifier, although it also introduces the GROUPS window frame unit. + @Test + public void testGroupsAsIdentifier() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT groups FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM groups", true); + assertSqlCanBeParsedAndDeparsed("SELECT a AS groups FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT a groups FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE mytable (groups INT)", true); + } + + // sample and extend are non-reserved in the common dialects and reach the + // parser as ordinary table and column names in downstream rewriting tools. + @Test + public void testSampleExtendAsIdentifier() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT sample, extend FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT mytable.extend FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE extend = 'x'", true); + assertSqlCanBeParsedAndDeparsed("UPDATE mytable SET sample = 1, extend = 'x' WHERE id = 1", + true); + assertSqlCanBeParsedAndDeparsed("INSERT INTO mytable (sample, extend) VALUES (1, 'x')", + true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM sample", true); + assertSqlCanBeParsedAndDeparsed("DELETE FROM sample WHERE extend = 'x'", true); + assertSqlCanBeParsedAndDeparsed("INSERT INTO sample (extend) VALUES ('x')", true); + assertSqlCanBeParsedAndDeparsed("SELECT a AS sample, b AS extend FROM mytable", true); + } + + @ParameterizedTest + @ValueSource(strings = {"exists", "sample", "extend", "SaMpLe", "ExTeNd"}) + void testSampleExtendImplicitAliases(String keyword) throws JSQLParserException { + // keyword is a non-reserved keyword in the common dialects and reaches the + // parser as an ordinary column name in downstream rewriting tools. + assertSqlCanBeParsedAndDeparsed("SELECT " + keyword + " FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT mytable.\" + keyword + \" FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE \" + keyword + \" = 1", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable ORDER BY \" + keyword + \"", true); + assertSqlCanBeParsedAndDeparsed("UPDATE mytable SET \" + keyword + \" = 1 WHERE id = 1", + true); + assertSqlCanBeParsedAndDeparsed("INSERT INTO mytable (\" + keyword + \") VALUES (1)", true); + assertSqlCanBeParsedAndDeparsed("SELECT a AS \" + keyword + \" FROM mytable", true); + } + + // the guarded keyword form keeps its operator meaning + @Test + public void testExistsPredicateStaysPredicate() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT EXISTS (SELECT 1 FROM x) FROM mytable", true); + assertInstanceOf(ExistsExpression.class, select.getSelectItem(0).getExpression()); + PlainSelect negated = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM mytable WHERE NOT EXISTS (SELECT 1 FROM x)", true); + assertInstanceOf(ExistsExpression.class, + assertInstanceOf(NotExpression.class, negated.getWhere()).getExpression()); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE EXISTS (SELECT 1 FROM x)", + true); + } + + @ParameterizedTest + @ValueSource(strings = {"exists", "ExIsTs"}) + void testExistsImplicitAliases(String keyword) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT a " + keyword + " FROM mytable " + keyword, true); + assertEquals(keyword, select.getSelectItem(0).getAlias().getName()); + assertEquals(keyword, select.getFromItem().getAlias().getName()); + assertEquals("a", assertInstanceOf(Column.class, + select.getSelectItem(0).getExpression()).getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"exists", "ExIsTs", "sample", "extend", "SaMpLe", "ExTeNd"}) + void testExistsCte(String keyword) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "WITH " + keyword + "(x) AS (SELECT 1) SELECT x FROM " + keyword, true); + WithItem withItem = select.getWithItemsList().get(0); + assertEquals(keyword, withItem.getAliasName()); + assertNotNull(withItem.getParenthesedStatement()); + assertNull(withItem.getExpression()); + assertEquals(keyword, assertInstanceOf(Table.class, select.getFromItem()).getName()); + assertSqlCanBeParsedAndDeparsed( + "WITH RECURSIVE " + keyword + "(x) AS (SELECT 1) SELECT * FROM " + keyword, true); + } + + @ParameterizedTest + @ValueSource(strings = {"exists", "ExIsTs", "sample", "extend", "SaMpLe", "ExTeNd"}) + void testExistsExpressionContexts(String keyword) throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT ALL " + keyword + " FROM mytable"); + assertEquals(keyword, assertInstanceOf(Column.class, + select.getSelectItem(0).getExpression()).getColumnName()); + assertSqlCanBeParsedAndDeparsed("SELECT CASE " + keyword + + " WHEN 1 THEN " + keyword + " ELSE 0 END FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("UPDATE " + keyword + " SET a = 1", true); + } + + @ParameterizedTest + @ValueSource(strings = {"exists", "ExIsTs", "sample", "extend", "SaMpLe", "ExTeNd"}) + void testExistsTernaryOperand(String keyword) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT flag ? " + keyword + " : 0 FROM mytable", true); + TernaryExpression ternary = assertInstanceOf(TernaryExpression.class, + select.getSelectItem(0).getExpression()); + assertEquals(keyword, assertInstanceOf(Column.class, ternary.getThenExpression()) + .getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"SAMPLE (10)", "SAMPLE BLOCK (10) SEED (1)", + "SAMPLE 0.1 OFFSET 1000", "USING SAMPLE 10 PERCENT (BERNOULLI)", + "TABLESAMPLE SYSTEM (10)"}) + void testSampleClauseStaysClause(String clause) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM mytable " + clause, true); + Table table = assertInstanceOf(Table.class, select.getFromItem()); + assertNull(table.getAlias()); + SampleClause sample = table.getSampleClause(); + assertNotNull(sample); + assertEquals(" " + clause, sample.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"sample", "extend", "SaMpLe", "ExTeNd"}) + void testSampleExtendPipeExpression(String keyword) throws JSQLParserException { + FromQuery query = (FromQuery) assertSqlCanBeParsedAndDeparsed( + "FROM " + keyword + " |> EXTEND " + keyword + " AS x", true); + SelectPipeOperator operator = assertInstanceOf(SelectPipeOperator.class, + query.getPipeOperators().get(0)); + assertEquals("EXTEND", operator.getOperatorName()); + assertEquals(keyword, assertInstanceOf(Column.class, + operator.getSelectItems().get(0).getExpression()).getColumnName()); + assertSqlCanBeParsedAndDeparsed( + "FROM mytable |> EXTEND " + keyword + "(x) AS x", true); + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT ALL " + keyword + "(x) FROM mytable"); + assertEquals(keyword, assertInstanceOf(Function.class, + select.getSelectItem(0).getExpression()).getName()); + } + + @Test + void testExistsColumnWithOracleJoin() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM mytable WHERE exists(+) = 1", true); + EqualsTo equals = assertInstanceOf(EqualsTo.class, select.getWhere()); + assertEquals("exists", assertInstanceOf(Column.class, equals.getLeftExpression()) + .getColumnName()); + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_JOIN_RIGHT, + ((Column) equals.getLeftExpression()).getOldOracleJoinSyntax()); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT 1", "VALUES (1)"}) + void testExistsUnparenthesizedSubSelect(String query) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT EXISTS " + query, true, p -> p.withUnparenthesizedSubSelects(true)); + assertInstanceOf(ExistsExpression.class, select.getSelectItem(0).getExpression()); + PlainSelect where = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM mytable WHERE EXISTS " + query, true, + p -> p.withUnparenthesizedSubSelects(true)); + assertInstanceOf(ExistsExpression.class, where.getWhere()); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "t.a", "[1]", "+1", "(1)", "(+1)", ":arr", "limit", "offset"}) + void testExistsBareOperandStaysPredicate(String operand) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT EXISTS " + operand + " FROM t", true); + ExistsExpression exists = assertInstanceOf(ExistsExpression.class, + select.getSelectItem(0).getExpression()); + assertEquals(operand, exists.getRightExpression().toString()); + PlainSelect where = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE EXISTS " + operand, true); + assertInstanceOf(ExistsExpression.class, where.getWhere()); + } + + @Test + void testExistsNamedParameterInTernary() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT flag ? EXISTS :arr : 0 FROM t", true); + TernaryExpression ternary = assertInstanceOf(TernaryExpression.class, + select.getSelectItem(0).getExpression()); + ExistsExpression exists = assertInstanceOf(ExistsExpression.class, + ternary.getThenExpression()); + assertEquals("arr", assertInstanceOf(JdbcNamedParameter.class, + exists.getRightExpression()).getName()); + } + + @Test + void testExistsMapKey() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT MAP {exists: 1}", true); + MapExpression map = assertInstanceOf(MapExpression.class, + select.getSelectItem(0).getExpression()); + assertEquals("exists", assertInstanceOf(Column.class, map.getEntries().get(0).getKey()) + .getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"IN (1, 2)", "NOT IN (1, 2)"}) + void testExistsColumnInCondition(String suffix) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE exists " + suffix, true); + InExpression in = assertInstanceOf(InExpression.class, select.getWhere()); + assertEquals("exists", assertInstanceOf(Column.class, in.getLeftExpression()) + .getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"ORDER BY id", "ORDER SIBLINGS BY id", "GROUP BY id", "LIMIT 1", + "LIMIT 1.0", "LIMIT ALL", "LIMIT NULL", "LIMIT ?", "LIMIT $1", "LIMIT :n", "LIMIT &n", + "OFFSET 1", "OFFSET 1.0", "OFFSET ?", "OFFSET $1", "OFFSET :n", "OFFSET &n"}) + void testExistsColumnBeforeClause(String suffix) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE exists " + suffix, true); + assertEquals("exists", assertInstanceOf(Column.class, select.getWhere()).getColumnName()); + if (suffix.startsWith("LIMIT")) { + assertNotNull(select.getLimit()); + assertEquals(suffix, select.getLimit().toString().trim()); + } else if (suffix.startsWith("OFFSET")) { + assertNotNull(select.getOffset()); + assertEquals(suffix, select.getOffset().toString().trim()); + } + } + + @Test + void testExistsBeforeNestedTernary() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT flag ? exists : b ? c : d FROM t", true); + TernaryExpression ternary = assertInstanceOf(TernaryExpression.class, + select.getSelectItem(0).getExpression()); + assertEquals("exists", assertInstanceOf(Column.class, ternary.getThenExpression()) + .getColumnName()); + assertInstanceOf(TernaryExpression.class, ternary.getElseExpression()); + PlainSelect mapSelect = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT MAP {exists: b ? c : d}", true); + MapExpression map = assertInstanceOf(MapExpression.class, + mapSelect.getSelectItem(0).getExpression()); + assertEquals("exists", assertInstanceOf(Column.class, map.getEntries().get(0).getKey()) + .getColumnName()); + assertInstanceOf(TernaryExpression.class, map.getEntries().get(0).getValue()); + } + + // ERRORS introduces Exasol's IMPORT/EXPORT error clauses but is an ordinary name elsewhere, + // e.g. a DuckDB column holding the errors of an upstream step. + @Test + public void testErrorsAsIdentifier() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT errors FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT t.errors AS errors FROM mytable t", true); + assertSqlCanBeParsedAndDeparsed("WITH errors AS (SELECT 1) SELECT * FROM errors", true); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE mytable (errors VARCHAR)", true); + } + + // Dialect-specific clause words (Exasol IMPORT/EXPORT, ClickHouse PREWHERE/SETTINGS, CREATE + // FUNCTION ... RETURNS) are ordinary names in other dialects, e.g. PostgreSQL or DuckDB. + @Test + public void testDialectClauseWordsAsIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "SELECT csv, fbv, file, prewhere, returns, script, settings, verify FROM mytable", + true); + assertSqlCanBeParsedAndDeparsed("SELECT t.file, t.settings FROM mytable t", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM settings", true); + assertSqlCanBeParsedAndDeparsed("WITH file AS (SELECT 1) SELECT * FROM file", true); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE mytable (csv VARCHAR, settings JSON)", true); + } + + @Test + public void testClickHouseClausesAfterTable() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT a FROM mytable PREWHERE b = 1 WHERE c = 2", true); + assertSqlCanBeParsedAndDeparsed("SELECT a FROM mytable SETTINGS max_threads = 8", true); + } + + // MySQL 8.4 and PostgreSQL 18 accept these keywords as unquoted column and table names. + public static Stream serverKeywordNames() { + return Stream.of("absent", "any", "between", "current", "exclude", "force", "full", + "grouping", "if", "import", "offset", "only", "prior", "returning", "set", "some", + "sql_no_cache", "unbounded", "unknown", "value", "values", "xmlserialize") + .flatMap(keyword -> Stream.of(Arguments.of(keyword, null), + Arguments.of(keyword, Dialect.MYSQL), + Arguments.of(keyword, Dialect.POSTGRESQL))); + } + + private static Statement assertRoundTrip(String sql, Dialect dialect) + throws JSQLParserException { + Consumer consumer = parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, consumer); + assertEquals(statement.toString(), + CCJSqlParserUtil.parse(statement.toString(), consumer).toString()); + return statement; + } + + @ParameterizedTest(name = "{0} ({1})") + @MethodSource("serverKeywordNames") + void testServerKeywordAsColumnAndTableName(String keyword, Dialect dialect) + throws JSQLParserException { + CreateTable createTable = (CreateTable) assertRoundTrip( + "CREATE TABLE mytable (id INT, " + keyword + " INT)", dialect); + assertEquals(keyword, createTable.getColumnDefinitions().get(1).getColumnName()); + + Alter alter = (Alter) assertRoundTrip( + "ALTER TABLE mytable ADD COLUMN " + keyword + " INT", dialect); + assertEquals(keyword, + alter.getAlterExpressions().get(0).getColDataTypeList().get(0).getColumnName()); + + // A keyword table name made CREATE TABLE fall back to an UnsupportedStatement + CreateTable namedTable = (CreateTable) assertRoundTrip( + "CREATE TABLE " + keyword + " (a INT)", dialect); + assertEquals(keyword, namedTable.getTable().getName()); + + assertRoundTrip("SELECT " + keyword + ", t." + keyword + " AS " + keyword + + " FROM mytable t WHERE " + keyword + " = 1 ORDER BY " + keyword, dialect); + assertRoundTrip("SELECT a FROM " + keyword + " WHERE a = 1", dialect); + assertRoundTrip("UPDATE mytable SET " + keyword + " = 1", dialect); + assertRoundTrip("INSERT INTO mytable (" + keyword + ") VALUES (1)", dialect); + } + + @Test + void testTableAndColumnKeepTheirOptionalClauses() throws JSQLParserException { + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE IF NOT EXISTS if (a INT)", true); + assertTrue(createTable.isIfNotExists()); + assertEquals("if", createTable.getTable().getName()); + + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE IF EXISTS value ADD COLUMN IF NOT EXISTS set INT", true); + assertTrue(alter.isUseTableIfExists()); + assertEquals("value", alter.getTable().getName()); + assertEquals("set", + alter.getAlterExpressions().get(0).getColDataTypeList().get(0).getColumnName()); + } + + @Test + void testJoinAndIndexHintKeywordsStayClauses() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM a FULL JOIN b ON a.id = b.id", true); + assertNull(((Table) select.getFromItem()).getAlias()); + assertTrue(select.getJoins().get(0).isFull()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM a full FULL OUTER JOIN b ON full.id = b.id", true); + assertEquals("full", select.getFromItem().getAlias().getName()); + Join join = select.getJoins().get(0); + assertTrue(join.isFull() && join.isOuter()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM mytable FORCE INDEX (idx) WHERE a = 1", true, + parser -> parser.withDialect(Dialect.MYSQL)); + Table table = (Table) select.getFromItem(); + assertNull(table.getAlias()); + assertNotNull(table.getIndexHint()); + } + + @Test + void testReturningAndAbsentStayClauses() throws JSQLParserException { + Delete delete = (Delete) assertSqlCanBeParsedAndDeparsed( + "DELETE FROM mytable RETURNING id", true); + assertNull(delete.getTable().getAlias()); + assertNotNull(delete.getReturningClause()); + + Insert insert = (Insert) assertSqlCanBeParsedAndDeparsed( + "INSERT INTO mytable SELECT a FROM other RETURNING id", true); + assertNull(((PlainSelect) insert.getSelect()).getFromItem().getAlias()); + assertNotNull(insert.getReturningClause()); + + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_ARRAY(a ABSENT ON NULL RETURNING JSON) FROM mytable", true); + JsonFunction function = (JsonFunction) select.getSelectItem(0).getExpression(); + assertEquals(1, function.getExpressions().size()); + assertEquals(JsonAggregateOnNullType.ABSENT, function.getOnNullType()); + assertEquals("JSON", function.getReturningType().getDataType()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAY(RETURNING JSON)", + true); + function = (JsonFunction) select.getSelectItem(0).getExpression(); + assertTrue(function.getExpressions().isEmpty()); + assertEquals("JSON", function.getReturningType().getDataType()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(ABSENT ON NULL)", + true); + function = (JsonFunction) select.getSelectItem(0).getExpression(); + assertTrue(function.getKeyValuePairs().isEmpty()); + assertEquals(JsonAggregateOnNullType.ABSENT, function.getOnNullType()); + } + + @Test + void testModifierAndOperatorKeywordsStayKeywords() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT SQL_NO_CACHE a FROM mytable", true); + assertTrue(select.getMySqlSelectModifiers().contains(MySqlSelectModifier.SQL_NO_CACHE)); + assertEquals("a", ((Column) select.getSelectItem(0).getExpression()).getColumnName()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT PRIOR ename FROM emp CONNECT BY PRIOR empno = mgr", true); + assertInstanceOf(ConnectByPriorOperator.class, select.getSelectItem(0).getExpression()); + EqualsTo connect = (EqualsTo) select.getOracleHierarchical().getConnectExpression(); + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + connect.getOraclePriorPosition()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT SUM(a) OVER (ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)" + + " FROM mytable", + true); + WindowElement frame = + ((AnalyticExpression) select.getSelectItem(0).getExpression()).getWindowElement(); + assertEquals(WindowOffset.Type.PRECEDING, frame.getRange().getStart().getType()); + assertNull(frame.getRange().getStart().getExpression()); + assertEquals(WindowOffset.Type.CURRENT, frame.getRange().getEnd().getType()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT XMLSERIALIZE(CONTENT x AS VARCHAR(100)) FROM mytable WHERE a IS NOT UNKNOWN", + true); + assertInstanceOf(XMLSerializeExpr.class, select.getSelectItem(0).getExpression()); + assertInstanceOf(IsUnknownExpression.class, select.getWhere()); + } + + @Test + void testPostgreSqlOnlyStaysKeyword() throws JSQLParserException { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE ONLY public.mytable ADD COLUMN a INT", true); + assertTrue(alter.isUseOnly()); + assertEquals("public.mytable", alter.getTable().getFullyQualifiedName()); + + PlainSelect select = + (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT * FROM ONLY mytable", true); + assertTrue(select.isUsingOnly()); + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX idx ON ONLY mytable (a)", true); + assertTrue(createIndex.isOnly()); + + // Still unsupported forms must not be read as a table named only + for (String sql : new String[] {"DELETE FROM ONLY mytable", + "SELECT * FROM a JOIN ONLY b ON a.id = b.id", "SELECT * FROM a, ONLY b", + "UPDATE a SET x = 1 FROM ONLY b"}) { + assertFalse(readsTableNamedOnly(sql), sql); + } + } + + private static boolean readsTableNamedOnly(String sql) { + try { + return new TablesNamesFinder<>().getTables(CCJSqlParserUtil.parse(sql)).stream() + .anyMatch("only"::equalsIgnoreCase); + } catch (JSQLParserException exception) { + return false; + } + } + + @Test + void testExcludeColumnAndPathType() throws JSQLParserException { + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE mytable (exclude INT, p path, EXCLUDE USING gist (p WITH &&))", true, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals("exclude", createTable.getColumnDefinitions().get(0).getColumnName()); + assertEquals("path", + createTable.getColumnDefinitions().get(1).getColDataType().getDataType()); + assertEquals(1, createTable.getTableElements(ExcludeConstraint.class).size()); + + // PATH still introduces the path of a JSON_TABLE column without a type + assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM JSON_TABLE(doc, '$' COLUMNS (a PATH '$.a', b INT PATH '$.b')) jt", + true); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/MySqlSetStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/MySqlSetStatementTest.java new file mode 100644 index 0000000000..1b44e79c1e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/MySqlSetStatementTest.java @@ -0,0 +1,122 @@ +/*- + * #%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.statement; + +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.SetStatement.AssignmentOperator; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MySqlSetStatementTest { + private SetStatement parse(String sql) throws Exception { + return (SetStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + @ParameterizedTest + @ValueSource(strings = {"SET @a := 1", "SET @a=1, @b:=2", "SET @a:=1, @b=2, @c:=3", + "SET @@session.sql_mode := ''", "SET SESSION sql_mode = ''", + "SET @a = (@b := 2), @c := 3", "SET CHARACTER SET utf8mb4", + "SET CHARACTER SET 'utf8mb4'", "SET CHARACTER SET DEFAULT", "SET CHARSET utf8mb4", + "SET CHARSET DEFAULT", "SET NAMES utf8mb4 COLLATE utf8mb4_bin", + "SET CHARSET DEFAULT, @a=1", "SET @a=1, CHARACTER SET utf8mb4"}) + void preservesSetSyntax(String sql) throws Exception { + SetStatement statement = parse(sql); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), parse(output.toString()).toString()); + } + + @Test + void exposesEachAssignmentAndOperator() throws Exception { + SetStatement statement = parse("SET @a=1, @b:=2, @c=3"); + assertEquals(3, statement.getCount()); + assertEquals("@b", statement.getName(1).toString()); + assertEquals(AssignmentOperator.COLON_EQUALS, statement.getAssignmentOperator(1)); + assertEquals(1, statement.getExpressions(1).size()); + assertInstanceOf(LongValue.class, statement.getExpressions(1).get(0)); + statement.setExpressions(1, new ExpressionList<>(new LongValue(9))); + statement.setAssignmentOperator(2, AssignmentOperator.COLON_EQUALS); + assertEquals("SET @a = 1, @b := 9, @c := 3", statement.toString()); + statement.setUseEqual(1, true); + assertEquals(AssignmentOperator.EQUALS, statement.getAssignmentOperator(1)); + assertTrue(statement.isUseEqual(1)); + statement.remove(0); + assertEquals("SET @b = 9, @c := 3", statement.toString()); + } + + @Test + void exposesCharsetSelectionAlongsideAssignments() throws Exception { + SetStatement statement = parse("SET @a=1, CHARACTER SET utf8mb4, @b:=2"); + assertEquals(3, statement.getCount()); + assertEquals("CHARACTER SET", statement.getName(1)); + assertEquals(AssignmentOperator.NONE, statement.getAssignmentOperator(1)); + assertEquals(AssignmentOperator.COLON_EQUALS, statement.getAssignmentOperator(2)); + } + + @Test + void keepsNestedAssignmentInItsValue() throws Exception { + SetStatement statement = parse("SET @a = (@b := 2), @c := 3"); + assertEquals(2, statement.getCount()); + assertEquals("@a", statement.getName().toString()); + assertEquals(1, statement.getExpressions().size()); + assertEquals("(@b := 2)", statement.getExpressions().get(0).toString()); + } + + @Test + void retainsPostgresqlValueListsAndLegacyApi() throws Exception { + SetStatement statement = (SetStatement) CCJSqlParserUtil.parse( + "SET search_path = public, extensions", p -> p.withDialect(Dialect.POSTGRESQL)); + assertEquals(1, statement.getCount()); + assertEquals(2, statement.getExpressions().size()); + assertEquals("SET search_path = public, extensions", statement.toString()); + SetStatement legacy = new SetStatement("x", new ExpressionList<>(new LongValue(1))); + assertEquals(AssignmentOperator.EQUALS, legacy.getAssignmentOperator()); + legacy.setUseEqual(false); + assertEquals("SET x 1", legacy.toString()); + legacy.setAssignmentOperator(AssignmentOperator.COLON_EQUALS); + assertEquals("SET x := 1", legacy.toString()); + } + + @Test + void visitsAllAssignmentValues() throws Exception { + List values = new java.util.ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + values.add(value.getValue()); + return getBuilder().append(value.getValue() + 10); + } + }; + StringBuilder output = new StringBuilder(); + parse("SET @a:=1, @b=2").accept( + new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(List.of(1L, 2L), values); + assertEquals("SET @a := 11, @b = 12", output.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"SET @a :=", "SET @a := 1,", "SET CHARACTER SET", + "SET CHARACTER SET utf8mb4 COLLATE utf8mb4_bin", "SET CHARSET DEFAULT,"}) + void rejectsIncompleteAssignmentsAndInvalidCharsetClauses(String sql) { + assertThrows(Exception.class, () -> parse(sql)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/OracleBlockTest.java b/src/test/java/net/sf/jsqlparser/statement/OracleBlockTest.java new file mode 100644 index 0000000000..07ddbd5924 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/OracleBlockTest.java @@ -0,0 +1,182 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.JdbcNamedParameter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.execute.Execute; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.OracleVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class OracleBlockTest { + private static OracleBlock parse(String sql) throws Exception { + OracleBlock block = + (OracleBlock) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.ORACLE)); + StringBuilder output = new StringBuilder(); + block.accept(new StatementDeParser(output)); + assertEquals(block.toString(), output.toString()); + assertEquals(block.toString(), CCJSqlParserUtil + .parse(output.toString(), p -> p.withDialect(Dialect.ORACLE)).toString()); + return block; + } + + @Test + void supportsIssue2007BindCalls() throws Exception { + OracleBlock block = + parse("BEGIN modifyParams(:BP_00000_v, :BP_00001_v, :BP_00012_v); END;"); + Execute call = assertInstanceOf(Execute.class, block.getStatements().get(0)); + assertEquals(Execute.ExecType.IMPLICIT, call.getExecType()); + assertEquals("modifyParams", call.getName()); + assertEquals(3, call.getExprList().size()); + List names = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(JdbcNamedParameter parameter, S context) { + assertEquals("ctx", context); + names.add(parameter.getName()); + return null; + } + }; + block.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), "ctx"); + assertEquals(List.of("BP_00000_v", "BP_00001_v", "BP_00012_v"), names); + assertTrue(block.getFeatures().isOpaque()); + assertTrue(block.getFeatures().getUnresolvedReferences().contains("modifyparams")); + assertThrows(UnsupportedOperationException.class, + () -> new TablesNamesFinder().getTables(block)); + } + + @Test + void supportsIssue1786AndMaintainerNestedExample() throws Exception { + OracleBlock simple = parse( + "DECLARE num NUMBER; BEGIN num := 10; dbms_output.put_line('The number is ' || num); END;"); + assertEquals("num", simple.getDeclarations().get(0).getName()); + assertEquals("NUMBER", simple.getDeclarations().get(0).getDataType().toString()); + OracleAssignment assignment = + assertInstanceOf(OracleAssignment.class, simple.getStatements().get(0)); + assertEquals("num", assignment.getTarget().toString()); + assertEquals("10", assignment.getValue().toString()); + OracleBlock nested = parse( + "DECLARE l_message VARCHAR2(100) := 'Hello'; BEGIN DECLARE l_message2 VARCHAR2(100) := l_message || ' World!'; BEGIN DBMS_OUTPUT.put_line(l_message2); END; EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.put_line(DBMS_UTILITY.format_error_stack); END;"); + assertInstanceOf(OracleBlock.class, nested.getStatements().get(0)); + assertEquals(List.of("OTHERS"), nested.getExceptionHandlers().get(0).getExceptions()); + assertEquals("DBMS_OUTPUT.put_line", + ((Execute) nested.getExceptionHandlers().get(0).getStatements().get(0)).getName()); + } + + @ParameterizedTest + @ValueSource(strings = { + "BEGIN p; p(); schema.pkg.p(1, 'text'); p(arg => 1); NULL; END;", + "DECLARE n CONSTANT NUMBER := 1; s VARCHAR2(10) NOT NULL DEFAULT 'a'; BEGIN n2 := n; END;", + "BEGIN :out_value := 1; rec.field := 2; END;", + "BEGIN NULL; EXCEPTION WHEN NO_DATA_FOUND OR TOO_MANY_ROWS THEN NULL; WHEN OTHERS THEN p; END;", + "BEGIN BEGIN NULL; END; BEGIN NULL; END; END;" + }) + void supportsBlockForms(String sql) throws Exception { + parse(sql); + } + + @Test + void visitsMutationsAcrossDeclarationsBodyAndHandlers() throws Exception { + OracleBlock block = parse( + "DECLARE n NUMBER := 1; BEGIN n := 2; EXCEPTION WHEN OTHERS THEN n := 3; END;"); + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("ctx", context); + values.add(value.getValue()); + return null; + } + }; + block.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), "ctx"); + assertEquals(List.of(1L, 2L, 3L), values); + assertFalse(block.getFeatures().modifiesData()); + block.getDeclarations().get(0).setInitializer(new LongValue(10)); + ((OracleAssignment) block.getExceptionHandlers().get(0).getStatements().get(0)) + .setValue(new LongValue(30)); + StringBuilder output = new StringBuilder(); + ExpressionDeParser deparser = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + assertEquals("ctx", context); + return getBuilder().append(value.getValue() + 1); + } + }; + block.accept(new StatementDeParser(deparser, new SelectDeParser(), output), "ctx"); + assertTrue(output.toString().contains("NUMBER := 11")); + assertTrue(output.toString().contains("n := 31")); + parse(output.toString()); + } + + @Test + void findsSqlTablesInBothMainBodyAndHandlers() throws Exception { + OracleBlock block = parse( + "DECLARE n NUMBER := (SELECT max(id) FROM source); BEGIN INSERT INTO target SELECT id FROM source; EXCEPTION WHEN OTHERS THEN INSERT INTO audit VALUES (1); END;"); + assertEquals(Set.of("source", "target", "audit"), new TablesNamesFinder().getTables(block)); + assertTrue(block.getFeatures().modifiesData()); + assertFalse(block.getFeatures().returnsResultSet()); + } + + @Test + void retainsScriptBoundariesAndExistingDialects() throws Exception { + assertEquals(3, CCJSqlParserUtil.parseStatements("BEGIN p; END; BEGIN q; END; SELECT 1;", + p -> p.withDialect(Dialect.ORACLE)).size()); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse("BEGIN p(1); END;")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("DECLARE n NUMBER; BEGIN n := 1; END;", + p -> p.withDialect(Dialect.SQLSERVER))); + assertInstanceOf(DeclareStatement.class, CCJSqlParserUtil.parse("DECLARE @n int = 1", + p -> p.withDialect(Dialect.SQLSERVER))); + assertInstanceOf(Block.class, CCJSqlParserUtil.parse("BEGIN SELECT 1; END;")); + } + + @ParameterizedTest + @ValueSource(strings = {"BEGIN RAISE; END;", "BEGIN LOOP; END;", "DECLARE BEGIN NULL; END;", + "DECLARE n NUMBER BEGIN NULL; END;", + "BEGIN p(1) END;", "BEGIN NULL;", "BEGIN END;", "BEGIN p(1,); END;", + "DECLARE n CONSTANT NUMBER; BEGIN NULL; END;", + "DECLARE n NUMBER NOT NULL; BEGIN NULL; END;", + "BEGIN NULL; EXCEPTION WHEN OTHERS THEN END;"}) + void rejectsIncompleteBlocks(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void validatesOracleBlockCapability() { + String sql = "DECLARE n NUMBER := 1; BEGIN n := 2; p(n); END;"; + assertTrue(new Validation(CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.ORACLE) + .getConfiguration(), List.of(OracleVersion.values()[0]), sql).validate().isEmpty()); + assertFalse( + new Validation( + CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.ORACLE) + .getConfiguration(), + List.of(new FeaturesAllowed(Feature.block)), sql).validate().isEmpty()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/OracleReturningIntoTest.java b/src/test/java/net/sf/jsqlparser/statement/OracleReturningIntoTest.java new file mode 100644 index 0000000000..d6abe7ab12 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/OracleReturningIntoTest.java @@ -0,0 +1,199 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.JdbcNamedParameter; +import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.UserVariable; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.merge.Merge; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class OracleReturningIntoTest { + static Stream dmlStatements() { + return Stream.of("INSERT INTO employees (id, salary) VALUES (1, 100)", + "UPDATE employees SET salary = salary + 100 WHERE id = 1", + "DELETE FROM employees WHERE id = 1", + "MERGE INTO employees e USING changes c ON (e.id = c.id) " + + "WHEN MATCHED THEN UPDATE SET e.salary = c.salary"); + } + + @ParameterizedTest + @MethodSource("dmlStatements") + void preservesNamedOutputBinds(String dml) throws JSQLParserException { + String sql = dml + " RETURNING id, salary INTO :out_id, :out_salary"; + for (Statement statement : parseBoth(sql)) { + ReturningClause clause = returningClause(statement); + assertFalse(clause.isBulkCollect()); + assertEquals(2, clause.getDataItems().size()); + assertEquals("out_id", + assertInstanceOf(JdbcNamedParameter.class, clause.getDataItems().get(0)) + .getName()); + assertEquals("out_salary", + assertInstanceOf(JdbcNamedParameter.class, clause.getDataItems().get(1)) + .getName()); + roundTrip(statement); + } + } + + @ParameterizedTest + @MethodSource("dmlStatements") + void preservesBulkCollectionAndTargets(String dml) throws JSQLParserException { + for (String targets : List.of("ids, salaries", ":ids, :salaries")) { + String sql = dml + " RETURNING id, salary BULK COLLECT INTO " + targets; + for (Statement statement : parseBoth(sql)) { + ReturningClause clause = returningClause(statement); + assertTrue(clause.isBulkCollect()); + assertEquals(2, clause.getDataItems().size()); + assertEquals(targets.startsWith(":"), + clause.getDataItems().get(0) instanceof JdbcNamedParameter); + assertTrue(statement.toString().contains(" BULK COLLECT INTO ")); + roundTrip(statement); + } + } + } + + @Test + void countsJdbcOutputParametersAfterInputParameters() throws JSQLParserException { + for (Statement statement : parseBoth("UPDATE employees SET salary = ? WHERE id = ? " + + "RETURNING id, salary INTO ?, ?")) { + ReturningClause clause = returningClause(statement); + assertFalse(clause.isBulkCollect()); + assertEquals(3, + assertInstanceOf(JdbcParameter.class, clause.getDataItems().get(0)).getIndex()); + assertEquals(4, + assertInstanceOf(JdbcParameter.class, clause.getDataItems().get(1)).getIndex()); + roundTrip(statement); + } + } + + @ParameterizedTest + @ValueSource(strings = {"out_id", "record.out_id", "@out_id"}) + void preservesExistingOutputTargets(String target) throws JSQLParserException { + Statement statement = CCJSqlParserUtil + .parse("INSERT INTO employees (id) VALUES (1) RETURNING id INTO " + target); + Object actual = returningClause(statement).getDataItems().get(0); + Class expectedType = target.startsWith("@") ? UserVariable.class : Table.class; + assertInstanceOf(expectedType, actual); + assertEquals(target, actual.toString()); + roundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"bulk", "AS bulk", "\"bulk\"", "AS collect"}) + void keepsAliasesBeforeBulkCollect(String alias) throws JSQLParserException { + for (Statement statement : parseBoth("UPDATE employees SET salary = 100 " + + "RETURNING salary " + alias + " BULK COLLECT INTO salaries")) { + ReturningClause clause = returningClause(statement); + assertTrue(clause.isBulkCollect()); + assertEquals(alias.replace("AS ", ""), clause.get(0).getAlias().getName()); + roundTrip(statement); + } + } + + @Test + void keepsBulkAndCollectAsOrdinaryIdentifiersAndAliases() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil + .parse("SELECT bulk, collect, salary bulk, salary collect FROM employees"); + assertEquals("bulk", select.getSelectItem(0).getExpression().toString()); + assertEquals("collect", select.getSelectItem(1).getExpression().toString()); + assertEquals("bulk", select.getSelectItem(2).getAlias().getName()); + assertEquals("collect", select.getSelectItem(3).getAlias().getName()); + roundTrip(select); + } + + @ParameterizedTest + @ValueSource(strings = {"bulk", "collect"}) + void keepsAliasesBeforeOrdinaryInto(String alias) throws JSQLParserException { + for (Statement statement : parseBoth("DELETE FROM employees RETURNING id " + alias + + " INTO out_id")) { + ReturningClause clause = returningClause(statement); + assertFalse(clause.isBulkCollect()); + assertEquals(alias, clause.get(0).getAlias().getName()); + roundTrip(statement); + } + } + + @Test + void allowsChangingCollectionModeThroughTheAst() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse( + "DELETE FROM employees RETURNING id BULK COLLECT INTO ids"); + ReturningClause clause = returningClause(statement); + clause.setBulkCollect(false); + assertFalse(statement.toString().contains("BULK COLLECT")); + roundTrip(statement); + clause.setBulkCollect(true); + assertTrue(statement.toString().contains("BULK COLLECT")); + roundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"INTO", "INTO :", "INTO :out_id,", "BULK COLLECT INTO", + "BULK COLLECT INTO ids,", "BULK COLLECT", "INTO :out_id + 1"}) + void rejectsIncompleteClauses(String clause) { + String sql = "DELETE FROM employees RETURNING id " + clause; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.ORACLE))); + } + + private static List parseBoth(String sql) throws JSQLParserException { + return List.of(TestUtils.assertSqlCanBeParsedAndDeparsed(sql), + CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.ORACLE))); + } + + private static ReturningClause returningClause(Statement statement) { + if (statement instanceof Insert) { + return ((Insert) statement).getReturningClause(); + } + if (statement instanceof Update) { + return ((Update) statement).getReturningClause(); + } + if (statement instanceof Delete) { + return ((Delete) statement).getReturningClause(); + } + return ((Merge) statement).getReturningClause(); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(statement.toString(), buffer.toString()); + Statement reparsed = CCJSqlParserUtil.parse(buffer.toString()); + assertEquals(buffer.toString(), reparsed.toString()); + if (!(statement instanceof PlainSelect)) { + ReturningClause before = returningClause(statement); + ReturningClause after = returningClause(reparsed); + assertEquals(before.isBulkCollect(), after.isBulkCollect()); + assertEquals(before.getDataItems().size(), after.getDataItems().size()); + for (int i = 0; i < before.getDataItems().size(); i++) { + assertEquals(before.getDataItems().get(i).getClass(), + after.getDataItems().get(i).getClass()); + assertEquals(before.getDataItems().get(i).toString(), + after.getDataItems().get(i).toString()); + } + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/PostgreSqlCopyTest.java b/src/test/java/net/sf/jsqlparser/statement/PostgreSqlCopyTest.java new file mode 100644 index 0000000000..451e4a6906 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/PostgreSqlCopyTest.java @@ -0,0 +1,100 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlCopyTest { + @ParameterizedTest + @ValueSource(strings = { + "COPY users (email, name) FROM STDIN WITH (FORMAT csv, HEADER true)", + "COPY users TO STDOUT WITH (FORMAT csv, DELIMITER ';', NULL '')", + "COPY users TO STDOUT WITH (FORMAT csv, FORCE_QUOTE (email, name))", + "COPY users TO STDOUT WITH (FORMAT csv, FORCE_QUOTE *)", + "COPY (SELECT email FROM users) TO STDOUT WITH (FORMAT csv, HEADER true)", + "COPY users FROM STDIN WITH (FORMAT csv, HEADER MATCH, FORCE_NULL (name))", + "COPY users FROM STDIN WITH (FORMAT csv, ON_ERROR ignore, ENCODING 'UTF8')", + "COPY users TO '/tmp/export.csv' WITH (FORMAT csv)", + "COPY users TO STDOUT (FORMAT csv, HEADER true)", + "COPY users FROM STDIN", + "COPY users TO STDOUT WITH", + "COPY (WITH c AS (SELECT email FROM users) SELECT email FROM c) TO STDOUT WITH (FORMAT csv)", + "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT WITH (FORMAT csv)" + }) + void preservesCopyOptionsAndWithKeyword(String sql) throws JSQLParserException { + CopyStatement copy = parse(sql); + assertEquals(sql.contains(" WITH ") || sql.endsWith(" WITH"), copy.isWithKeyword()); + assertRoundTrip(copy); + assertEquals(copy.toString(), CCJSqlParserUtil.parse(copy.toString()).toString()); + } + + @Test + void optionalWithKeywordCanBeEditedWithoutChangingTheOptions() throws JSQLParserException { + CopyStatement copy = parse("COPY users TO STDOUT WITH (FORMAT csv, HEADER true)"); + assertTrue(copy.isWithKeyword()); + copy.setWithKeyword(false); + assertEquals("COPY users TO STDOUT (FORMAT csv, HEADER true)", copy.toString()); + assertFalse(parse(copy.toString()).isWithKeyword()); + copy.withWithKeyword(true); + assertRoundTrip(copy); + copy.setOptions(null); + assertEquals("COPY users TO STDOUT WITH", copy.toString()); + assertRoundTrip(copy); + } + + @Test + void passesQueryExpressionsToTheConfiguredDeparser() throws JSQLParserException { + CopyStatement copy = parse("COPY (SELECT 7 FROM users WHERE id > 8) " + + "TO STDOUT WITH (FORMAT csv)"); + StringBuilder buffer = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + copy.accept(new StatementDeParser(expressions, new SelectDeParser(), buffer), null); + assertEquals("COPY (SELECT 107 FROM users WHERE id > 108) TO STDOUT WITH (FORMAT csv)", + buffer.toString()); + assertRoundTrip(parse(buffer.toString())); + } + + @ParameterizedTest + @ValueSource(strings = {"WITH ()", "WITH (FORMAT csv,)", "WITH (,FORMAT csv)", + "WITH FORMAT csv", "WITH (FORMAT csv,, HEADER true)", "WITH (FORCE_QUOTE (email)"}) + void rejectsMalformedWithOptions(String options) { + assertThrows(JSQLParserException.class, () -> parse("COPY users TO STDOUT " + options)); + } + + private static CopyStatement parse(String sql) throws JSQLParserException { + return (CopyStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(CopyStatement copy) throws JSQLParserException { + StringBuilder buffer = new StringBuilder(); + copy.accept(new StatementDeParser(buffer), null); + assertEquals(copy.toString(), buffer.toString()); + assertEquals(copy.toString(), parse(buffer.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/PostgreSqlPrepareTest.java b/src/test/java/net/sf/jsqlparser/statement/PostgreSqlPrepareTest.java new file mode 100644 index 0000000000..be9642db25 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/PostgreSqlPrepareTest.java @@ -0,0 +1,95 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlPrepareTest { + @ParameterizedTest + @ValueSource(strings = { + "PREPARE p(bigint) AS SELECT * FROM users WHERE id = $1", + "PREPARE p(bigint[], text) AS SELECT * FROM users WHERE id = ANY($1) AND name = $2", + "PREPARE p(pg_catalog.int4, numeric(10, 2)) AS SELECT $1 + $2", + "PREPARE \"prepared query\"(timestamp with time zone, double precision) AS SELECT $1, $2", + "PREPARE p(unknown) AS SELECT $1::text", + "PREPARE p(bigint, text) AS INSERT INTO users(id, name) VALUES ($1, $2)", + "PREPARE p(text, bigint) AS UPDATE users SET name = $1 WHERE id = $2", + "PREPARE p(bigint) AS DELETE FROM users WHERE id = $1", + "PREPARE p AS SELECT * FROM users WHERE id = $1" + }) + void preservesDeclaredTypesAndNestedStatements(String sql) throws JSQLParserException { + PrepareStatement prepare = parse(sql); + assertRoundTrip(prepare); + assertEquals(prepare.toString(), CCJSqlParserUtil.parse(prepare.toString()).toString()); + } + + @Test + void exposesMutableTypesAndRetainsInferredForm() throws JSQLParserException { + PrepareStatement prepare = parse("PREPARE p(bigint, text) AS SELECT $1, $2"); + assertEquals(2, prepare.getParameterTypes().size()); + assertEquals("bigint", prepare.getParameterTypes().get(0).getBaseTypeName()); + prepare.getParameterTypes().get(0).setDataType("integer"); + assertEquals("PREPARE p(integer, text) AS SELECT $1, $2", prepare.toString()); + assertRoundTrip(prepare); + prepare.setParameterTypes(null); + assertEquals("PREPARE p AS SELECT $1, $2", prepare.toString()); + assertNull(parse(prepare.toString()).getParameterTypes()); + assertRoundTrip(new PrepareStatement("p", prepare.getStatement())); + } + + @Test + void passesNestedExpressionsToTheConfiguredDeparser() throws JSQLParserException { + PrepareStatement prepare = + parse("PREPARE p(bigint) AS SELECT $1 + 7 FROM users WHERE id > 8"); + StringBuilder buffer = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + prepare.accept(new StatementDeParser(expressions, new SelectDeParser(), buffer), null); + assertEquals("PREPARE p(bigint) AS SELECT $1 + 107 FROM users WHERE id > 108", + buffer.toString()); + assertRoundTrip(parse(buffer.toString())); + } + + @ParameterizedTest + @ValueSource(strings = {"PREPARE p() AS SELECT 1", "PREPARE p(bigint,) AS SELECT $1", + "PREPARE p(bigint text) AS SELECT $1", "PREPARE p(bigint) SELECT $1"}) + void rejectsMalformedTypeLists(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static PrepareStatement parse(String sql) throws JSQLParserException { + return (PrepareStatement) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(PrepareStatement prepare) throws JSQLParserException { + StringBuilder buffer = new StringBuilder(); + prepare.accept(new StatementDeParser(buffer), null); + assertEquals(prepare.toString(), buffer.toString()); + assertEquals(prepare.toString(), parse(buffer.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/PostgreSqlSetToTest.java b/src/test/java/net/sf/jsqlparser/statement/PostgreSqlSetToTest.java new file mode 100644 index 0000000000..f031781a38 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/PostgreSqlSetToTest.java @@ -0,0 +1,165 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.SetStatement.AssignmentOperator; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlSetToTest { + private static SetStatement parse(String sql) throws JSQLParserException { + return (SetStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @ValueSource(strings = {"SET search_path TO my_schema, public", + "SET LOCAL statement_timeout TO 5000", "SET SESSION application_name TO 'worker'", + "SET datestyle TO postgres, dmy", "SET client_encoding TO DEFAULT", + "SET app.user_name TO 'reader'", "SET \"TimeZone\" TO 'UTC'", + "SET standard_conforming_strings TO on", "SET enable_seqscan TO off", + "SET enable_hashjoin TO true", "SET enable_mergejoin TO false", + "SET search_path TO off, on"}) + void parsesScopeNamesAndValueLists(String sql) throws Exception { + SetStatement set = parse(sql); + assertEquals(1, set.getCount()); + assertEquals(AssignmentOperator.TO, set.getAssignmentOperator()); + assertFalse(set.isUseEqual()); + assertRoundTrip(set, sql); + SetStatement generic = (SetStatement) CCJSqlParserUtil.parse(sql); + assertEquals(AssignmentOperator.TO, generic.getAssignmentOperator()); + } + + @Test + void distinguishesOneValueListFromSeveralAssignments() throws Exception { + SetStatement set = parse("SET LOCAL search_path TO my_schema, public, '$user'"); + assertEquals("LOCAL", set.getEffectParameter()); + assertEquals("search_path", set.getName()); + assertEquals(1, set.getCount()); + assertEquals(3, set.getExpressions().size()); + assertEquals("my_schema", set.getExpressions().get(0).toString()); + assertEquals("public", set.getExpressions().get(1).toString()); + assertEquals("$user", ((StringValue) set.getExpressions().get(2)).getValue()); + } + + @Test + void keepsLegacyAssignmentApiAndSeparators() throws Exception { + SetStatement constructed = new SetStatement("x", new ExpressionList<>(new LongValue(1))); + assertTrue(constructed.isUseEqual()); + constructed.setAssignmentOperator(AssignmentOperator.TO); + assertRoundTrip(constructed, "SET x TO 1"); + assertFalse(constructed.isUseEqual()); + constructed.setUseEqual(true); + assertEquals("SET x = 1", constructed.toString()); + constructed.setUseEqual(false); + assertEquals("SET x 1", constructed.toString()); + SetStatement assignments = (SetStatement) CCJSqlParserUtil.parse("SET @a=1, @b:=2, @c=3", + p -> p.withDialect(Dialect.MYSQL)); + assertEquals(3, assignments.getCount()); + assertEquals(AssignmentOperator.EQUALS, assignments.getAssignmentOperator(0)); + assertEquals(AssignmentOperator.COLON_EQUALS, assignments.getAssignmentOperator(1)); + assertEquals("SET @a = 1, @b := 2, @c = 3", assignments.toString()); + assertRoundTrip(parse("SET TIME ZONE 'UTC'"), "SET Time Zone 'UTC'"); + } + + @Test + void keepsExistingOnExpressionsOutsideToAssignments() throws Exception { + for (String sql : List.of("SET v = on + 1", "SET v = 1, on")) { + for (Dialect dialect : List.of(Dialect.POSTGRESQL, Dialect.MYSQL, Dialect.SQLSERVER)) { + SetStatement set = (SetStatement) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(dialect)); + assertEquals(AssignmentOperator.EQUALS, set.getAssignmentOperator()); + assertRoundTrip(set, sql); + } + } + } + + @Test + void visitsValuesInToEqualsAndSpaceForms() throws Exception { + List values = new ArrayList<>(); + StatementVisitorAdapter visitor = new StatementVisitorAdapter<>( + new SelectVisitorAdapter<>(new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + values.add(value.getValue()); + return null; + } + })); + parse("SET x TO 1, 2").accept(visitor, null); + CCJSqlParserUtil.parse("SET x = 3, y 4").accept(visitor, null); + assertEquals(List.of(1L, 2L, 3L, 4L), values); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 10); + } + }; + parse("SET x TO 1, 2").accept( + new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals("SET x TO 11, 12", output.toString()); + } + + @Test + void retainsFollowingStatements() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + "SET LOCAL search_path TO my_schema, public; SELECT 1; SET x = 3;", + p -> p.withDialect(Dialect.POSTGRESQL)); + assertEquals(3, statements.size()); + assertEquals(AssignmentOperator.TO, + ((SetStatement) statements.get(0)).getAssignmentOperator()); + assertEquals(AssignmentOperator.EQUALS, + ((SetStatement) statements.get(2)).getAssignmentOperator()); + } + + @ParameterizedTest + @ValueSource(strings = {"SET search_path TO", "SET search_path TO public,", + "SET x TO = 1", "SET x TO 1, y TO 2", "SET x = 1, y TO 2", + "SET @x TO 1", "SET TIME ZONE TO 'UTC'"}) + void rejectsMalformedOrMultipleParameterToAssignments(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void doesNotEnableToForExplicitMySqlDialect() { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse("SET x TO 1", + p -> p.withDialect(Dialect.MYSQL))); + } + + private static void assertRoundTrip(SetStatement set, String expected) throws Exception { + assertEquals(expected, set.toString()); + StringBuilder output = new StringBuilder(); + set.accept(new StatementDeParser(output), null); + assertEquals(expected, output.toString()); + SetStatement reparsed = parse(output.toString()); + assertEquals(set.getAssignmentOperator(), reparsed.getAssignmentOperator()); + assertEquals(set.getEffectParameter(), reparsed.getEffectParameter()); + assertEquals(set.getName(), reparsed.getName()); + assertEquals(set.getExpressions().toString(), reparsed.getExpressions().toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/PostgresqlExplainTest.java b/src/test/java/net/sf/jsqlparser/statement/PostgresqlExplainTest.java new file mode 100644 index 0000000000..f0cef624dd --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/PostgresqlExplainTest.java @@ -0,0 +1,112 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.ExplainStatement.Option; +import net.sf.jsqlparser.statement.ExplainStatement.OptionType; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgresqlExplainTest { + @ParameterizedTest + @ValueSource(strings = { + "EXPLAIN SELECT 1", + "EXPLAIN ANALYZE VERBOSE SELECT 1", + "EXPLAIN (ANALYZE, VERBOSE, COSTS, BUFFERS) SELECT 1", + "EXPLAIN (ANALYZE TRUE, VERBOSE ON, COSTS OFF, BUFFERS FALSE) SELECT 1", + "EXPLAIN (ANALYZE 1, COSTS 0, FORMAT TEXT) SELECT 1", + "EXPLAIN (FORMAT XML) SELECT 1", + "EXPLAIN (FORMAT JSON) SELECT 1", + "EXPLAIN (FORMAT YAML) SELECT 1", + "EXPLAIN (ANALYZE, TIMING, SUMMARY, SETTINGS, WAL) SELECT 1", + "EXPLAIN (GENERIC_PLAN, SERIALIZE NONE, MEMORY) SELECT 1", + "EXPLAIN (SERIALIZE TEXT, GENERIC_PLAN FALSE) SELECT 1", + "EXPLAIN (ANALYZE, SERIALIZE BINARY) SELECT 1", + "EXPLAIN (SERIALIZE) SELECT 1", + "EXPLAIN (ANALYZE OFF, ANALYZE ON) SELECT 1", + "EXPLAIN (ANALYZE) WITH t AS (SELECT * FROM source) SELECT * FROM t", + "EXPLAIN (ANALYZE FALSE) INSERT INTO t VALUES (1)", + "EXPLAIN (COSTS OFF) UPDATE t SET n = 2", + "EXPLAIN (FORMAT JSON) DELETE FROM t WHERE n = 1", + "EXPLAIN (COSTS) VALUES (1), (2)", + "EXPLAIN (SELECT 1)" + }) + void roundTrip(String sql) throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "EXPLAIN () SELECT 1", + "EXPLAIN ANALYZE TRUE SELECT 1", + "EXPLAIN VERBOSE TRUE SELECT 1", + "EXPLAIN BUFFERS SELECT 1", + "EXPLAIN COSTS SELECT 1", + "EXPLAIN FORMAT XML SELECT 1", + "EXPLAIN VERBOSE ANALYZE SELECT 1", + "EXPLAIN ANALYZE ANALYZE SELECT 1", + "EXPLAIN PLAN FOR SELECT 1", + "EXPLAIN (FORMAT) SELECT 1", + "EXPLAIN (FORMAT LONGTEXT) SELECT 1", + "EXPLAIN (ANALYZE 2) SELECT 1", + "EXPLAIN (ANALYZE MAYBE) SELECT 1", + "EXPLAIN (SERIALIZE JSON) SELECT 1", + "EXPLAIN (UNKNOWN_OPTION) SELECT 1", + "EXPLAIN (ANALYZE,) SELECT 1", + "EXPLAIN (ANALYZE VERBOSE) SELECT 1", + "EXPLAIN some_table" + }) + void rejectInvalidPostgresqlOptions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL).withUnsupportedStatements(false))); + } + + @Test + void preserveRepetitionsAndLegacyMapAccess() throws JSQLParserException { + ExplainStatement explain = (ExplainStatement) TestUtils.assertSqlCanBeParsedAndDeparsed( + "EXPLAIN (ANALYZE OFF, COSTS, ANALYZE ON) SELECT * FROM accounts"); + assertTrue(explain.isParenthesizedOptions()); + assertEquals(3, explain.getOptionList().size()); + assertEquals(2, explain.getOptions().size()); + assertEquals("ON", explain.getOption(OptionType.ANALYZE).getValue()); + assertEquals("ON", explain.getOptions().get(OptionType.ANALYZE).getValue()); + explain.getOptionList().clear(); + explain.getOptions().clear(); + assertEquals(3, explain.getOptionList().size()); + explain.addOption(new Option(OptionType.ANALYZE).withValue("FALSE")); + assertEquals(3, explain.getOptionList().size()); + assertEquals("FALSE", explain.getOption(OptionType.ANALYZE).getValue()); + assertEquals(List.of("accounts"), new TablesNamesFinder().getTableList(explain)); + } + + @Test + void constructOptionsAndKeepOtherDialects() throws JSQLParserException { + ExplainStatement explain = new ExplainStatement(CCJSqlParserUtil.parse("SELECT 1")); + assertNull(explain.getOptions()); + explain.setParenthesizedOptions(true); + explain.addOption(new Option(OptionType.GENERIC_PLAN)); + explain.addOption(new Option(OptionType.FORMAT).withValue("TEXT")); + TestUtils.assertStatementCanBeDeparsedAs(explain, + "EXPLAIN (GENERIC_PLAN, FORMAT TEXT) SELECT 1"); + TestUtils.assertSqlCanBeParsedAndDeparsed("EXPLAIN PLAN FOR SELECT 1"); + TestUtils.assertSqlCanBeParsedAndDeparsed("EXPLAIN BUFFERS FALSE SELECT 1"); + TestUtils.assertSqlCanBeParsedAndDeparsed("SUMMARIZE accounts"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/RefreshMaterializedViewStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/RefreshMaterializedViewStatementTest.java index 4ebba56c7f..ad0561352b 100644 --- a/src/test/java/net/sf/jsqlparser/statement/RefreshMaterializedViewStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/RefreshMaterializedViewStatementTest.java @@ -10,9 +10,21 @@ package net.sf.jsqlparser.statement; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.refresh.RefreshMode; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.RefreshMaterializedViewStatementDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * @@ -21,8 +33,83 @@ public class RefreshMaterializedViewStatementTest { + @ParameterizedTest + @ValueSource(strings = {"REFRESH MATERIALIZED VIEW my_view", + "REFRESH MATERIALIZED VIEW my_view WITH DATA", + "REFRESH MATERIALIZED VIEW my_view WITH NO DATA", + "REFRESH MATERIALIZED VIEW CONCURRENTLY my_view", + "REFRESH MATERIALIZED VIEW CONCURRENTLY my_view WITH DATA"}) + public void testRefreshModes(String sql) throws JSQLParserException { + RefreshMaterializedViewStatement statement = + (RefreshMaterializedViewStatement) assertSqlCanBeParsedAndDeparsed(sql); + StringBuilder output = new StringBuilder(); + new RefreshMaterializedViewStatementDeParser(output).deParse(statement); + assertEquals(sql, output.toString()); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + } + + @Test + void preservesModeWhenVisitingViewName() throws JSQLParserException { + RefreshMaterializedViewStatement statement = + (RefreshMaterializedViewStatement) CCJSqlParserUtil + .parse("REFRESH MATERIALIZED VIEW old_schema.mv WITH NO DATA"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser(); + SelectDeParser selects = new SelectDeParser(expressions, output) { + @Override + public StringBuilder visit(Table table, S context) { + assertEquals("context", context); + getBuilder().append("new_schema.\"renamed view\""); + return getBuilder(); + } + }; + StringBuilder original = new StringBuilder(); + StatementDeParser deparser = new StatementDeParser(expressions, selects, original); + deparser.setBuilder(output); + assertSame(output, statement.accept(deparser, "context")); + assertEquals("", original.toString()); + assertEquals("REFRESH MATERIALIZED VIEW new_schema.\"renamed view\" WITH NO DATA", + output.toString()); + RefreshMaterializedViewStatement reparsed = + (RefreshMaterializedViewStatement) CCJSqlParserUtil.parse(output.toString()); + assertEquals(RefreshMode.WITH_NO_DATA, reparsed.getRefreshMode()); + assertEquals("new_schema", reparsed.getView().getSchemaName()); + } + + @Test + void statementDeparserUsesReplacementOutputBuffer() throws JSQLParserException { + StringBuilder original = new StringBuilder("old output"); + StringBuilder replacement = new StringBuilder(); + StatementDeParser deparser = new StatementDeParser(original); + deparser.setBuilder(replacement); + RefreshMaterializedViewStatement statement = + (RefreshMaterializedViewStatement) CCJSqlParserUtil + .parse("REFRESH MATERIALIZED VIEW schema_name.mv WITH NO DATA"); + assertSame(replacement, statement.accept(deparser, null)); + assertEquals("old output", original.toString()); + assertEquals(statement.toString(), replacement.toString()); + } + + @Test + void respectsReplacementOutputBuffer() throws JSQLParserException { + StringBuilder original = new StringBuilder(); + StringBuilder replacement = new StringBuilder(); + RefreshMaterializedViewStatementDeParser deparser = + new RefreshMaterializedViewStatementDeParser(original); + deparser.setBuilder(replacement); + deparser.deParse((RefreshMaterializedViewStatement) CCJSqlParserUtil + .parse("REFRESH MATERIALIZED VIEW mv WITH NO DATA")); + assertEquals("", original.toString()); + assertEquals("REFRESH MATERIALIZED VIEW mv WITH NO DATA", replacement.toString()); + } + @Test - public void testSimpleUse() throws JSQLParserException { - assertSqlCanBeParsedAndDeparsed("REFRESH MATERIALIZED VIEW my_view"); + void rendersDefaultModeLikeOmittedMode() { + RefreshMaterializedViewStatement statement = new RefreshMaterializedViewStatement( + new Table("mv"), true, RefreshMode.DEFAULT); + StringBuilder output = new StringBuilder(); + new RefreshMaterializedViewStatementDeParser(output).deParse(statement); + assertEquals("REFRESH MATERIALIZED VIEW CONCURRENTLY mv", output.toString()); + assertEquals(statement.toString(), output.toString()); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java b/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java index f5cfad0f6d..8b23ebdc21 100644 --- a/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java @@ -9,19 +9,43 @@ */ package net.sf.jsqlparser.statement; -import net.sf.jsqlparser.JSQLParserException; -import net.sf.jsqlparser.parser.CCJSqlParserUtil; -import net.sf.jsqlparser.statement.select.PlainSelect; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.SpannerInterleaveIn; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.select.JoinHint; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.SampleClause; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; public class SerializationTest { + @Test + void serializeSelectWithLeadingComments() throws Exception { + PlainSelect original = (PlainSelect) CCJSqlParserUtil + .parse("/* keep orderby */ SELECT id FROM mytable"); + Assertions.assertEquals(List.of("/* keep orderby */"), original.getLeadingComments()); + PlainSelect copy = roundTrip(original); + Assertions.assertEquals("SELECT id FROM mytable", copy.toString()); + Assertions.assertNull(copy.getASTNode()); + Assertions.assertEquals(List.of(), copy.getLeadingComments()); + } + @Test void serializeWithItem() throws JSQLParserException, IOException, ClassNotFoundException { String sqlStr = @@ -49,4 +73,182 @@ void serializeWithItem() throws JSQLParserException, IOException, ClassNotFoundE "The deserialized object should be equal to the original"); } + @ParameterizedTest + @ValueSource(strings = {"LOOP", "HASH", "MERGE", "REMOTE"}) + void serializeSqlServerJoinHint(String keyword) throws Exception { + PlainSelect original = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.id FROM a INNER " + keyword + " JOIN b ON a.id = b.id"); + PlainSelect copy = roundTrip(original); + JoinHint hint = copy.getJoins().get(0).getJoinHint(); + Assertions.assertEquals(keyword, hint.getKeyword()); + Assertions.assertEquals(JoinHint.Position.BEFORE_JOIN, hint.getPosition()); + Assertions.assertEquals("b", ((Table) copy.getJoins().get(0).getFromItem()).getName()); + Assertions.assertEquals("a.id = b.id", + copy.getJoins().get(0).getOnExpressions().iterator().next().toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"shuffle", "BROADCAST"}) + void serializeDorisJoinHint(String keyword) throws Exception { + PlainSelect original = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.id FROM a LEFT JOIN [" + keyword + "] b USING (id)", false, + parser -> parser.withDialect(Dialect.DORIS)); + PlainSelect copy = roundTrip(original); + JoinHint hint = copy.getJoins().get(0).getJoinHint(); + Assertions.assertEquals(keyword, hint.getKeyword()); + Assertions.assertEquals(JoinHint.Position.AFTER_JOIN, hint.getPosition()); + Assertions.assertEquals("id", + copy.getJoins().get(0).getUsingColumns().get(0).getColumnName()); + Assertions.assertTrue(copy.getJoins().get(0).isLeft()); + } + + static Stream samplingClauses() { + return Stream.of( + Arguments.of("TABLESAMPLE BERNOULLI (10)", + new SampleClause("TABLESAMPLE", "BERNOULLI", 10L, null, null, null)), + Arguments.of("TABLESAMPLE SYSTEM (10.5 PERCENT) REPEATABLE (7)", + new SampleClause("TABLESAMPLE", "SYSTEM", 10.5, "PERCENT", 7L, null)), + Arguments.of("SAMPLE (99)", + new SampleClause("SAMPLE", null, 99L, null, null, null)), + Arguments.of("SAMPLE BLOCK (99.1) SEED (10.1)", + new SampleClause("SAMPLE", "BLOCK", 99.1, null, null, 10.1)), + Arguments.of("SAMPLE 0.1 OFFSET 1000", + new SampleClause("SAMPLE", null, 0.1, null, null, null, false, 1000L)), + Arguments.of("USING SAMPLE 10%", + new SampleClause("USING SAMPLE", null, 10L, "%", null, null, false, null)), + Arguments.of("USING SAMPLE 10 ROWS (system, 377)", + new SampleClause("USING SAMPLE", "SYSTEM", 10L, "ROWS", null, 377L, + false, null).setMethodInBrackets(true)), + Arguments.of("USING SAMPLE RESERVOIR (50 ROWS)", + new SampleClause("USING SAMPLE", "RESERVOIR", 50L, "ROWS", null, null))); + } + + @ParameterizedTest + @MethodSource("samplingClauses") + void serializeTableSampleClause(String clause, SampleClause expected) throws Exception { + PlainSelect original = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT id FROM events " + clause + " WHERE id > 0"); + assertSampleClause(expected, ((Table) original.getFromItem()).getSampleClause()); + PlainSelect copy = roundTrip(original); + assertSampleClause(expected, ((Table) copy.getFromItem()).getSampleClause()); + Assertions.assertEquals("events", ((Table) copy.getFromItem()).getName()); + Assertions.assertEquals("id > 0", copy.getWhere().toString()); + Assertions.assertEquals("id", copy.getSelectItem(0).toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * FROM (SELECT id FROM events) TABLESAMPLE SYSTEM (10 PERCENT)", + "SELECT * FROM (events) TABLESAMPLE SYSTEM (10 PERCENT)", + "SELECT * FROM events MATCH_RECOGNIZE (PATTERN (A) DEFINE A AS id > 0) " + + "TABLESAMPLE SYSTEM (10 PERCENT)" + }) + void serializeOtherSampleClauseOwners(String sql) throws Exception { + PlainSelect original = (PlainSelect) CCJSqlParserUtil.parse(sql); + PlainSelect copy = roundTrip(original); + assertSampleClause(new SampleClause("TABLESAMPLE", "SYSTEM", 10L, "PERCENT", null, null), + copy.getFromItem().getSampleClause()); + } + + @Test + void serializeSampleClauseAfterMutation() throws Exception { + PlainSelect original = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM events SAMPLE BLOCK (10) SEED (7)"); + SampleClause sample = original.getFromItem().getSampleClause(); + sample.setPercentageArgument(new BigDecimal("12.5")); + sample.setSeedArgument(new BigDecimal("7.5")); + PlainSelect copy = roundTrip(original); + SampleClause copiedSample = copy.getFromItem().getSampleClause(); + assertSampleClause(new SampleClause("SAMPLE", "BLOCK", new BigDecimal("12.5"), null, + null, new BigDecimal("7.5")), copiedSample); + Assertions.assertNotSame(sample, copiedSample); + sample.setSeedArgument(99L); + Assertions.assertEquals(new BigDecimal("7.5"), copiedSample.getSeedArgument()); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ON DELETE CASCADE", " ON DELETE NO ACTION"}) + void serializeSpannerInterleaveIn(String action) throws Exception { + CreateTable original = (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE child (id INT) PRIMARY KEY (id), INTERLEAVE IN PARENT parent" + + action); + CreateTable copy = roundTrip(original); + SpannerInterleaveIn interleave = copy.getSpannerInterleaveIn(); + Assertions.assertEquals("parent", interleave.getTable().getName()); + SpannerInterleaveIn.OnDelete expected = action.isEmpty() ? null + : action.endsWith("CASCADE") ? SpannerInterleaveIn.OnDelete.CASCADE + : SpannerInterleaveIn.OnDelete.NO_ACTION; + Assertions.assertEquals(expected, interleave.getOnDelete()); + Assertions.assertEquals("child", copy.getTable().getName()); + Assertions.assertEquals("id", copy.getColumnDefinitions().get(0).getColumnName()); + Assertions.assertEquals("INT", + copy.getColumnDefinitions().get(0).getColDataType().getDataType()); + original.getSpannerInterleaveIn().getTable().setName("changed"); + Assertions.assertEquals("parent", interleave.getTable().getName()); + } + + @Test + void serializeCombinedHelpers() throws Exception { + PlainSelect original = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.id FROM a TABLESAMPLE SYSTEM (10) INNER LOOP JOIN b " + + "ON a.id = b.id JOIN c ON b.id = c.id"); + PlainSelect copy = roundTrip(original); + Assertions.assertEquals("LOOP", copy.getJoins().get(0).getJoinHint().getKeyword()); + assertSampleClause(new SampleClause("TABLESAMPLE", "SYSTEM", 10L, null, null, null), + ((Table) copy.getFromItem()).getSampleClause()); + Assertions.assertNull(((Table) copy.getJoins().get(0).getFromItem()).getSampleClause()); + Assertions.assertNull(copy.getJoins().get(1).getJoinHint()); + Assertions.assertNull(((Table) copy.getJoins().get(1).getFromItem()).getSampleClause()); + } + + @Test + void serializeSelectWithoutHelpers() throws Exception { + PlainSelect original = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.id FROM a INNER JOIN b ON a.id = b.id WHERE b.id > 0"); + PlainSelect copy = roundTrip(original); + Assertions.assertNull(copy.getJoins().get(0).getJoinHint()); + Assertions.assertNull(((Table) copy.getFromItem()).getSampleClause()); + Assertions.assertEquals("b.id > 0", copy.getWhere().toString()); + } + + @Test + void serializeCreateTableWithoutInterleave() throws Exception { + CreateTable original = (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE ordinary (id INT)"); + CreateTable copy = roundTrip(original); + Assertions.assertNull(copy.getSpannerInterleaveIn()); + Assertions.assertEquals("ordinary", copy.getTable().getName()); + Assertions.assertEquals("id", copy.getColumnDefinitions().get(0).getColumnName()); + } + + private static void assertSampleClause(SampleClause expected, SampleClause actual) { + Assertions.assertNotNull(actual); + Assertions.assertEquals(expected.getKeyword(), actual.getKeyword()); + Assertions.assertEquals(expected.getMethod(), actual.getMethod()); + Assertions.assertEquals(expected.getPercentageArgument(), actual.getPercentageArgument()); + Assertions.assertEquals(expected.getPercentageUnit(), actual.getPercentageUnit()); + Assertions.assertEquals(expected.isArgumentInBrackets(), actual.isArgumentInBrackets()); + Assertions.assertEquals(expected.isMethodInBrackets(), actual.isMethodInBrackets()); + Assertions.assertEquals(expected.getOffsetArgument(), actual.getOffsetArgument()); + Assertions.assertEquals(expected.getRepeatArgument(), actual.getRepeatArgument()); + Assertions.assertEquals(expected.getSeedArgument(), actual.getSeedArgument()); + } + + @SuppressWarnings("unchecked") + private static T roundTrip(T original) + throws IOException, ClassNotFoundException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ObjectOutputStream out = new ObjectOutputStream(bytes)) { + out.writeObject(original); + } + T copy; + try (ObjectInputStream in = + new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) { + copy = (T) in.readObject(); + } + Assertions.assertNotSame(original, copy); + Assertions.assertEquals(original.toString(), copy.toString()); + return copy; + } + } diff --git a/src/test/java/net/sf/jsqlparser/statement/SetIdentityInsertTest.java b/src/test/java/net/sf/jsqlparser/statement/SetIdentityInsertTest.java new file mode 100644 index 0000000000..b83de91b51 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/SetIdentityInsertTest.java @@ -0,0 +1,170 @@ +/*- + * #%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.statement; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import net.sf.jsqlparser.util.validation.feature.SqlServerVersion; +import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation; +import net.sf.jsqlparser.util.validation.metadata.Named; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; +import net.sf.jsqlparser.util.validation.validator.StatementValidator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class SetIdentityInsertTest { + private SetIdentityInsertStatement parse(String sql) throws JSQLParserException { + return (SetIdentityInsertStatement) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.SQLSERVER).withUnsupportedStatements(false)); + } + + private void assertRoundTrip(SetIdentityInsertStatement set, String sql) throws Exception { + assertEquals(sql, set.toString()); + StringBuilder buffer = new StringBuilder(); + set.accept(new StatementDeParser(buffer), null); + assertEquals(sql, buffer.toString()); + SetIdentityInsertStatement reparsed = parse(buffer.toString()); + assertEquals(set.isOn(), reparsed.isOn()); + assertEquals(set.getTable().getFullyQualifiedName(), + reparsed.getTable().getFullyQualifiedName()); + } + + @ParameterizedTest + @ValueSource(strings = {"language", "country", "city", "address", "actor", "staff", + "store", "category", "film", "inventory", "customer", "rental", "payment"}) + void parsesSakilaIdentityDirectives(String table) throws Exception { + for (boolean on : new boolean[] {true, false}) { + String sql = "SET IDENTITY_INSERT " + table + (on ? " ON" : " OFF"); + SetIdentityInsertStatement set = parse(sql); + assertEquals(table, set.getTable().getName()); + assertEquals(on, set.isOn()); + assertRoundTrip(set, sql); + } + } + + @ParameterizedTest + @ValueSource(strings = {"dbo.actor", "sakila.dbo.actor", "sakila..actor", + "[sakila].[dbo].[order]", "[table with spaces]", "#staging"}) + void reusesQualifiedTableNames(String table) throws Exception { + SetIdentityInsertStatement set = parse("set identity_insert " + table + " on"); + assertEquals(table, set.getTable().getFullyQualifiedName()); + assertEquals(Collections.singleton(table), new TablesNamesFinder().getTables(set)); + assertRoundTrip(set, "SET IDENTITY_INSERT " + table + " ON"); + } + + @Test + void exposesEditableTargetAndStateWithoutClaimingDataChanges() throws Exception { + SetIdentityInsertStatement set = parse("SET IDENTITY_INSERT dbo.actor ON"); + set.getTable().setName("customer"); + set.setOn(false); + assertRoundTrip(set, "SET IDENTITY_INSERT dbo.customer OFF"); + set.setTable(new Table("replacement")); + assertRoundTrip(set, "SET IDENTITY_INSERT replacement OFF"); + assertEquals(Collections.singleton(StmtFeature.MODIFIES_SESSION), + set.getFeatures().getCertain()); + assertTrue(set.getFeatures().getUncertain().isEmpty()); + assertEquals(3, CCJSqlParserUtil.parseStatements( + "SET IDENTITY_INSERT dbo.actor ON; INSERT INTO dbo.actor VALUES (1); " + + "SET IDENTITY_INSERT dbo.actor OFF;", + p -> p.withDialect(Dialect.SQLSERVER)).size()); + } + + @Test + void traversesTheTargetWithVisitorContext() throws Exception { + SetIdentityInsertStatement set = parse("SET IDENTITY_INSERT dbo.actor ON"); + Object context = new Object(); + List visited = new ArrayList<>(); + FromItemVisitorAdapter tables = new FromItemVisitorAdapter() { + @Override + public Void visit(Table table, S value) { + assertSame(context, value); + visited.add(table); + return null; + } + }; + set.accept(new StatementVisitorAdapter(null, null, null, tables, null, null), + context); + assertEquals(Collections.singletonList(set.getTable()), visited); + + StringBuilder buffer = new StringBuilder(); + SelectDeParser selects = new SelectDeParser() { + @Override + public StringBuilder visit(Table table, S value) { + assertSame(context, value); + return getBuilder().append("mapped_table"); + } + }; + set.accept(new StatementDeParser(new ExpressionDeParser(), selects, buffer), context); + assertEquals("SET IDENTITY_INSERT mapped_table ON", buffer.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"SET IDENTITY_INSERT", "SET IDENTITY_INSERT ON", + "SET IDENTITY_INSERT dbo.actor", "SET IDENTITY_INSERT dbo.actor TRUE", + "SET IDENTITY_INSERT dbo.actor = ON", "SET IDENTITY_INSERT dbo.actor ON OFF", + "SET IDENTITY_INSERT server.sakila.dbo.actor ON", + "SET IDENTITY_INSERT dbo.actor AS a ON", "SET IDENTITY_INSERT @actor ON"}) + void rejectsMalformedDirectives(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void preservesGenericSetAndRequiresSqlServerDialect() throws Exception { + String sql = "SET IDENTITY_INSERT dbo.actor ON"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL))); + for (String generic : new String[] {"SET IDENTITY_INSERT = 1", + "SET IDENTITY_INSERT.value = 1", "SET @flag = 1"}) { + assertInstanceOf(SetStatement.class, CCJSqlParserUtil.parse(generic, + p -> p.withDialect(Dialect.SQLSERVER))); + } + } + + @Test + void validatesTheCapabilityAndExistingTargetTable() throws Exception { + SetIdentityInsertStatement set = parse("SET IDENTITY_INSERT dbo.actor OFF"); + List visited = new ArrayList<>(); + DatabaseMetaDataValidation metadata = name -> { + visited.add(name); + return true; + }; + StatementValidator validator = new StatementValidator(); + validator.setContext(new ValidationContext().setCapabilities( + Arrays.asList(SqlServerVersion.V2019, PostgresqlVersion.V10, metadata))); + set.accept(validator, null); + assertFalse(validator.getValidationErrors().containsKey(SqlServerVersion.V2019)); + assertFalse(validator.getValidationErrors().containsKey(metadata)); + ValidationTestAsserts.assertNotSupported( + validator.getValidationErrors().get(PostgresqlVersion.V10), + Feature.setIdentityInsert); + assertEquals(1, visited.size()); + assertEquals(NamedObject.table, visited.get(0).getNamedObject()); + assertEquals("dbo.actor", visited.get(0).getFqn()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/SetPasswordStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/SetPasswordStatementTest.java new file mode 100644 index 0000000000..6b19e84b83 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/SetPasswordStatementTest.java @@ -0,0 +1,126 @@ +/*- + * #%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.statement; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SetPasswordStatementTest { + + private static SetPasswordStatement parse(String sql, Dialect dialect) + throws JSQLParserException { + SetPasswordStatement statement = assertInstanceOf(SetPasswordStatement.class, TestUtils + .assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(dialect))); + assertEquals(sql, assertInstanceOf(SetPasswordStatement.class, CCJSqlParserUtil + .parse(statement.toString(), parser -> parser.withDialect(dialect))).toString()); + return statement; + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "MARIADB"}) + void setsPasswordForAccount(Dialect dialect) throws JSQLParserException { + SetPasswordStatement statement = parse("SET PASSWORD FOR 'app'@'%' = 'x'", dialect); + + assertEquals("app", statement.getAccount().getUser().getValue()); + assertEquals("%", statement.getAccount().getHost().getValue()); + assertEquals("x", ((StringValue) statement.getPassword()).getValue()); + assertFalse(statement.isRandom()); + + SetPasswordStatement current = parse("SET PASSWORD = 'x'", dialect); + assertNull(current.getAccount()); + } + + @Test + void generatesRandomPasswordAndRetainsCurrent() throws JSQLParserException { + SetPasswordStatement statement = parse( + "SET PASSWORD FOR app TO RANDOM REPLACE 'old' RETAIN CURRENT PASSWORD", + Dialect.MYSQL); + + assertTrue(statement.isRandom()); + assertNull(statement.getPassword()); + assertEquals("old", statement.getCurrentCredential().getValue()); + assertTrue(statement.isRetainCurrentPassword()); + + assertTrue(parse("SET PASSWORD TO RANDOM", Dialect.MYSQL).isRandom()); + assertTrue(parse("SET PASSWORD = 'new' REPLACE 'old'", Dialect.MYSQL) + .getCurrentCredential() != null); + } + + @Test + void keepsMariaDbPasswordFunction() throws JSQLParserException { + SetPasswordStatement statement = + parse("SET PASSWORD FOR 'app'@'%' = PASSWORD('x')", Dialect.MARIADB); + + assertEquals("PASSWORD", ((Function) statement.getPassword()).getName()); + } + + @Test + void keepsGenericAssignmentWithoutMySqlDialect() throws JSQLParserException { + for (String sql : List.of("SET PASSWORD = 'x'", "SET PASSWORD TO RANDOM")) { + assertInstanceOf(SetStatement.class, CCJSqlParserUtil.parse(sql), sql); + assertInstanceOf(SetStatement.class, + CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)), + sql); + } + // FOR names an account and is typed in every configuration + assertInstanceOf(SetPasswordStatement.class, TestUtils + .assertSqlCanBeParsedAndDeparsed("SET PASSWORD FOR 'app'@'%' = 'x'", false)); + } + + @Test + void buildsAndDeparses() { + SetPasswordStatement statement = new SetPasswordStatement() + .withAccount(new UserAccount().withUser(new StringValue("app"))) + .withPassword(new StringValue("new")).withRetainCurrentPassword(true); + + assertDeparse(statement, "SET PASSWORD FOR 'app' = 'new' RETAIN CURRENT PASSWORD"); + assertDeparse(statement.withRandom(true).withPassword(null), + "SET PASSWORD FOR 'app' TO RANDOM RETAIN CURRENT PASSWORD"); + } + + @Test + void reportsSchemaChangeWithoutTables() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("SET PASSWORD FOR app = 'x'"); + + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(TablesNamesFinder.findTables(statement.toString())).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"SET PASSWORD FOR app", "SET PASSWORD = 'x' REPLACE", + "SET PASSWORD TO 'x'", "SET PASSWORD FOR app TO RANDOM RETAIN PASSWORD", + "SET PASSWORD FOR app IDENTIFIED BY 'x' = 'y'"}) + void rejectsSyntaxOutsideTheStatementDefinition(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/SetRoleStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/SetRoleStatementTest.java new file mode 100644 index 0000000000..8b24d9bd16 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/SetRoleStatementTest.java @@ -0,0 +1,175 @@ +/*- + * #%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.statement; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.user.RoleSelection; +import net.sf.jsqlparser.statement.create.user.RoleSelection.Kind; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** MySQL SET ROLE and SET DEFAULT ROLE; PostgreSQL SET ROLE stays a {@link SetStatement}. */ +class SetRoleStatementTest { + + private static T parse(Class type, String sql, Dialect dialect) + throws JSQLParserException { + T statement = assertInstanceOf(type, TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(dialect))); + assertEquals(sql, assertInstanceOf(type, + CCJSqlParserUtil.parse(statement.toString(), parser -> parser.withDialect(dialect))) + .toString()); + return statement; + } + + private static List names(RoleSelection roles) { + return roles.getRoles().stream().map(UserAccount::toString).collect(Collectors.toList()); + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "MARIADB"}) + void typesEverySetRoleFormInMySqlDialects(Dialect dialect) throws JSQLParserException { + assertEquals(Kind.DEFAULT, + parse(SetRoleStatement.class, "SET ROLE DEFAULT", dialect).getRoles().getKind()); + assertEquals(Kind.NONE, + parse(SetRoleStatement.class, "SET ROLE NONE", dialect).getRoles().getKind()); + assertEquals(Kind.ALL, + parse(SetRoleStatement.class, "SET ROLE ALL", dialect).getRoles().getKind()); + + RoleSelection except = + parse(SetRoleStatement.class, "SET ROLE ALL EXCEPT 'writer', 'admin'@'%'", dialect) + .getRoles(); + assertEquals(Kind.ALL_EXCEPT, except.getKind()); + assertEquals(List.of("'writer'", "'admin'@'%'"), names(except)); + + RoleSelection roles = + parse(SetRoleStatement.class, "SET ROLE 'reader', app_role", dialect).getRoles(); + assertEquals(Kind.ROLES, roles.getKind()); + assertEquals(List.of("'reader'", "app_role"), names(roles)); + } + + @Test + void keepsPostgreSqlSetRole() throws JSQLParserException { + for (Dialect dialect : new Dialect[] {null, Dialect.POSTGRESQL}) { + for (String sql : List.of("SET ROLE app_admin", "SET ROLE 'reader'", "SET ROLE NONE", + "SET ROLE DEFAULT", "SET ROLE ALL", "SET LOCAL ROLE 'app'", + "SET SESSION ROLE app")) { + assertInstanceOf(SetStatement.class, CCJSqlParserUtil.parse(sql, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }), sql); + } + } + // variable assignments and scoped forms are not MySQL SET ROLE + for (String sql : List.of("SET role = 'x'", "SET LOCAL ROLE 'app'")) { + assertInstanceOf(SetStatement.class, + CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)), sql); + } + } + + @Test + void typesMySqlOnlyFormsWithoutDialect() throws JSQLParserException { + for (Dialect dialect : new Dialect[] {Dialect.ANSI_SQL, Dialect.POSTGRESQL}) { + assertEquals(Kind.ALL_EXCEPT, + parse(SetRoleStatement.class, "SET ROLE ALL EXCEPT 'writer'", dialect) + .getRoles().getKind()); + } + for (String sql : List.of("SET ROLE r1, 'r2'@'%'", "SET ROLE 'r'@localhost", + "SET ROLE r@'%'", "SET ROLE `r`@`%`")) { + assertInstanceOf(SetRoleStatement.class, + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false), sql); + } + // an unquoted r@h is one identifier, as before + assertInstanceOf(SetStatement.class, CCJSqlParserUtil.parse("SET ROLE r@h")); + } + + @Test + void setsDefaultRoles() throws JSQLParserException { + SetDefaultRoleStatement single = parse(SetDefaultRoleStatement.class, + "SET DEFAULT ROLE 'reader' TO 'app'@'%'", Dialect.MYSQL); + assertEquals(List.of("'reader'"), names(single.getRoles())); + assertEquals("app", single.getAccounts().get(0).getUser().getValue()); + assertFalse(single.isForKeyword()); + + SetDefaultRoleStatement all = parse(SetDefaultRoleStatement.class, + "SET DEFAULT ROLE ALL TO 'app'@'%', 'u2'@'%'", Dialect.MYSQL); + assertEquals(Kind.ALL, all.getRoles().getKind()); + assertEquals(2, all.getAccounts().size()); + + assertEquals(Kind.NONE, parse(SetDefaultRoleStatement.class, + "SET DEFAULT ROLE NONE TO app", Dialect.MYSQL).getRoles().getKind()); + // available without a dialect, as no generic SET form starts with SET DEFAULT ROLE + assertInstanceOf(SetDefaultRoleStatement.class, TestUtils + .assertSqlCanBeParsedAndDeparsed("SET DEFAULT ROLE r1, r2 TO app", false)); + } + + @Test + void setsMariaDbDefaultRole() throws JSQLParserException { + SetDefaultRoleStatement forUser = parse(SetDefaultRoleStatement.class, + "SET DEFAULT ROLE reader FOR 'app'@'localhost'", Dialect.MARIADB); + assertTrue(forUser.isForKeyword()); + assertEquals(1, forUser.getAccounts().size()); + + SetDefaultRoleStatement current = + parse(SetDefaultRoleStatement.class, "SET DEFAULT ROLE NONE", Dialect.MARIADB); + assertTrue(current.getAccounts().isEmpty()); + } + + @Test + void buildsAndDeparses() { + UserAccount reader = new UserAccount().withUser(new StringValue("reader")); + assertDeparse(new SetRoleStatement(new RoleSelection(Kind.ALL_EXCEPT).addRoles(reader)), + "SET ROLE ALL EXCEPT 'reader'"); + assertDeparse(new SetDefaultRoleStatement().withRoles(new RoleSelection(Kind.ROLES) + .addRoles(reader)).addAccounts(new UserAccount().withUser(new StringValue("app")) + .withHost(new StringValue("%"))), + "SET DEFAULT ROLE 'reader' TO 'app'@'%'"); + } + + @Test + void reportsStatementFeatures() throws JSQLParserException { + Statement setRole = + CCJSqlParserUtil.parse("SET ROLE ALL", parser -> parser.withDialect(Dialect.MYSQL)); + assertThat(setRole.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SESSION); + + Statement setDefaultRole = CCJSqlParserUtil.parse("SET DEFAULT ROLE ALL TO app"); + assertThat(setDefaultRole.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(TablesNamesFinder.findTables(setDefaultRole.toString())).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"SET ROLE ALL EXCEPT", "SET ROLE 'r' IDENTIFIED BY 'x'", + "SET DEFAULT ROLE DEFAULT TO app", "SET DEFAULT ROLE ALL EXCEPT r TO app", + "SET DEFAULT ROLE TO app", "SET DEFAULT ROLE ALL TO"}) + void rejectsSyntaxOutsideTheStatementDefinition(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java index 825a18def0..eb07f57d62 100644 --- a/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java @@ -9,9 +9,28 @@ */ package net.sf.jsqlparser.statement; -import net.sf.jsqlparser.JSQLParserException; +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; +import net.sf.jsqlparser.util.TablesNamesFinder; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * @@ -19,8 +38,154 @@ */ public class ShowColumnsStatementTest { + private static final List> DIALECTS = List.of(parser -> { + }, parser -> parser.withDialect(Dialect.MYSQL), + parser -> parser.withDialect(Dialect.MARIADB)); + + /** Parses without a dialect and with MySQL and MariaDB, then deparses and reparses. */ + private static ShowColumnsStatement roundTrip(String sql) throws JSQLParserException { + ShowColumnsStatement result = null; + for (Consumer dialect : DIALECTS) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect); + assertInstanceOf(ShowColumnsStatement.class, statement); + assertDeparse(statement, sql); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString()); + result = (ShowColumnsStatement) statement; + } + return result; + } + @Test public void testSimpleUse() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SHOW COLUMNS FROM mydatabase"); } + + @Test + void plainFormKeepsTheTableNameApi() throws JSQLParserException { + ShowColumnsStatement show = roundTrip("SHOW COLUMNS FROM mytable"); + + assertEquals("mytable", show.getTableName()); + assertEquals("mytable", show.getTable().getName()); + assertFalse(show.isExtended()); + assertFalse(show.isFull()); + assertFalse(show.isFieldsKeyword()); + assertEquals(SelectionMode.FROM, show.getTableSelectionMode()); + assertNull(show.getDbName()); + assertNull(show.getLikeExpression()); + assertNull(show.getWhereCondition()); + } + + @Test + void fullColumnsWithDatabaseAndLike() throws JSQLParserException { + ShowColumnsStatement show = roundTrip("SHOW FULL COLUMNS FROM gc20 FROM v LIKE 'n%'"); + + assertTrue(show.isFull()); + assertEquals("gc20", show.getTableName()); + assertEquals(SelectionMode.FROM, show.getDbSelectionMode()); + assertEquals("v", show.getDbName()); + assertEquals("n%", ((StringValue) show.getLikeExpression()).getValue()); + } + + @Test + void extendedFullFieldsInWithWhere() throws JSQLParserException { + ShowColumnsStatement show = + roundTrip("SHOW EXTENDED FULL FIELDS IN db.t IN `other` WHERE `Key` = 'PRI'"); + + assertTrue(show.isExtended()); + assertTrue(show.isFull()); + assertTrue(show.isFieldsKeyword()); + assertEquals(SelectionMode.IN, show.getTableSelectionMode()); + assertEquals("db", show.getTable().getSchemaName()); + assertEquals("db.t", show.getTableName()); + assertEquals(SelectionMode.IN, show.getDbSelectionMode()); + assertEquals("`other`", show.getDbName()); + assertEquals("`Key` = 'PRI'", show.getWhereCondition().toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW FIELDS FROM t", + "SHOW FULL FIELDS FROM gc20", + "SHOW COLUMNS IN t", + "SHOW COLUMNS FROM `db`.`t`", + "SHOW COLUMNS FROM t IN db", + "SHOW EXTENDED COLUMNS FROM t", + "SHOW COLUMNS FROM t LIKE 'a%'", + "SHOW FULL COLUMNS FROM t WHERE Comment <> ''" + }) + void parsesEveryMySqlForm(String sql) throws JSQLParserException { + roundTrip(sql); + } + + @Test + void keywordsAreCaseInsensitive() throws JSQLParserException { + assertEquals("SHOW FULL FIELDS IN t FROM db", + CCJSqlParserUtil.parse("show full fields in t from db").toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW COLUMNS IN SCHEMA s", + "SHOW COLUMNS IN TABLE t", + "SHOW COLUMNS LIKE 'x' IN TABLE t", + "SHOW COLUMNS FROM t NOT LIKE 'x'", + "SHOW COLUMNS FROM t FROM db LIMIT 3", + "SHOW FULL PROCESSLIST" + }) + void otherDialectFormsRemainUnsupported(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + assertInstanceOf(UnsupportedStatement.class, statement); + } + + @Test + void endsAtTheStatementBoundary() throws JSQLParserException { + Statements statements = + CCJSqlParserUtil.parseStatements("SHOW FULL COLUMNS FROM t;\nSELECT 1"); + + assertEquals(2, statements.size()); + assertInstanceOf(ShowColumnsStatement.class, statements.get(0)); + + IfElseStatement ifElse = (IfElseStatement) CCJSqlParserUtil + .parse("IF a = 1 SHOW COLUMNS FROM t ELSE SHOW TABLE STATUS"); + assertInstanceOf(ShowColumnsStatement.class, ifElse.getIfStatement()); + assertEquals("SHOW TABLE STATUS", ifElse.getElseStatement().toString()); + } + + @Test + void findsTheTable() throws JSQLParserException { + assertThat(TablesNamesFinder.findTables("SHOW FULL COLUMNS FROM s.t")) + .containsExactly("s.t"); + // the trailing FROM db names the database of the table + assertThat(TablesNamesFinder.findTables("SHOW COLUMNS FROM t FROM db")) + .containsExactly("db.t"); + } + + @Test + void legacyTableNameAccessors() { + ShowColumnsStatement show = new ShowColumnsStatement("db.t"); + + assertEquals("db", show.getTable().getSchemaName()); + assertEquals("db.t", show.getTableName()); + assertEquals("SHOW COLUMNS FROM db.t", show.toString()); + assertEquals("SHOW COLUMNS FROM t2", show.withTableName("t2").toString()); + show.setTableName(null); + assertNull(show.getTable()); + } + + @Test + void buildsAndDeparses() { + ShowColumnsStatement show = new ShowColumnsStatement(new Table("t")).withFull(true) + .withFieldsKeyword(true).withTableSelectionMode(SelectionMode.IN) + .withDbName("db").withLikeExpression(new StringValue("a%")); + + assertDeparse(show, "SHOW FULL FIELDS IN t FROM db LIKE 'a%'"); + } + + @Test + void reportsMetadataFeatures() throws JSQLParserException { + StatementFeatures features = + CCJSqlParserUtil.parse("SHOW FULL COLUMNS FROM t").getFeatures(); + + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/ShowStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/ShowStatementTest.java index 7f10338964..23830cbc9e 100644 --- a/src/test/java/net/sf/jsqlparser/statement/ShowStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/ShowStatementTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -43,6 +44,6 @@ void testShowCreateTable() throws JSQLParserException { String sqlStr = "show create table my_table"; Statement statement = assertSqlCanBeParsedAndDeparsed(sqlStr, true); - Assertions.assertTrue(statement instanceof UnsupportedStatement); + Assertions.assertTrue(statement instanceof ShowCreateStatement); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/SqlServerRoutineTest.java b/src/test/java/net/sf/jsqlparser/statement/SqlServerRoutineTest.java new file mode 100644 index 0000000000..c579a10124 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/SqlServerRoutineTest.java @@ -0,0 +1,177 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.create.function.CreateFunction; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.SqlServerVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlServerRoutineTest { + private static CreateFunctionalStatement parse(String sql) throws Exception { + CreateFunctionalStatement statement = + (CreateFunctionalStatement) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.SQLSERVER)); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(output.toString(), + p -> p.withDialect(Dialect.SQLSERVER)).toString()); + return statement; + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE", "ALTER", "CREATE OR ALTER"}) + void supportsIssue1978AndSharedOperations(String prefix) throws Exception { + CreateFunctionalStatement function = parse( + prefix + " FUNCTION getPayments() RETURNS TABLE AS RETURN SELECT * from Payments;"); + assertEquals(prefix.replace(' ', '_'), function.getOperation().name()); + assertTrue(function.getReturnType().isTable()); + assertNull(function.getReturnType().getTableElements()); + CreateFunctionalStatement procedure = parse(prefix + + " PROCEDURE SPPayment AS SET NOCOUNT ON; BEGIN SELECT * FROM Payments; END"); + assertEquals("PROCEDURE", procedure.getKind()); + assertNull(procedure.getReturnType()); + assertTrue(procedure.formatDeclaration().contains("NOCOUNT ON; BEGIN")); + assertTrue(procedure.getFeatures().modifiesSchema()); + assertThrows(UnsupportedOperationException.class, + () -> new TablesNamesFinder().getTables(procedure)); + } + + @Test + void exposesIssue715ReturnTableAndConstraintExpressions() throws Exception { + CreateFunctionalStatement statement = parse( + "CREATE OR ALTER FUNCTION dbo.f(@id int = 1) RETURNS @result TABLE (id int NOT NULL, amount decimal(10, 2), PRIMARY KEY (id), CHECK (amount > 0)) AS BEGIN INSERT INTO @result SELECT id, amount FROM payments; RETURN; END;"); + assertEquals("@result", statement.getReturnType().getTableVariable()); + assertEquals(4, statement.getReturnType().getTableElements().size()); + List columns = + statement.getReturnType().getTableElements(ColumnDefinition.class); + assertEquals(2, columns.size()); + assertEquals(2, statement.getReturnType().getTableElements(NamedConstraint.class).size()); + assertEquals("id", columns.get(0).getColumnName()); + List visited = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("ctx", context); + visited.add(value.getValue()); + return null; + } + }; + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "ctx"); + assertEquals(List.of(0L), visited); + columns.get(0).setColumnName("payment_id"); + statement.getReturnType().setTableVariable("@rows"); + assertTrue(statement.toString().contains("RETURNS @rows TABLE (payment_id int NOT NULL")); + CheckConstraint check = + statement.getReturnType().getTableElements(CheckConstraint.class).get(0); + check.setExpression(CCJSqlParserUtil.parseCondExpression("amount > 10")); + StringBuilder output = new StringBuilder(); + ExpressionDeParser deparser = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 1); + } + }; + statement.accept(new StatementDeParser(deparser, new SelectDeParser(), output)); + assertTrue(output.toString().contains("CHECK (amount > 11)")); + parse(output.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE FUNCTION dbo.f() RETURNS int AS BEGIN RETURN CASE WHEN 1 = 1 THEN 1 ELSE 0 END; END;", + "ALTER FUNCTION dbo.f() RETURNS @r TABLE (id int) AS BEGIN BEGIN INSERT INTO @r SELECT 1; END; RETURN; END;", + "CREATE FUNCTION dbo.f() RETURNS TABLE (id int) AS EXTERNAL NAME assembly.class.method;" + }) + void preservesScalarClrAndNestedBodies(String sql) throws Exception { + parse(sql); + } + + @Test + void retainsFollowingStatements() throws Exception { + for (String sql : List.of( + "ALTER FUNCTION f() RETURNS TABLE AS RETURN SELECT 1 AS id; SELECT 2;", + "ALTER FUNCTION f() RETURNS int AS BEGIN RETURN CASE WHEN 1=1 THEN 1 END; END; SELECT 2;")) { + assertEquals(2, CCJSqlParserUtil + .parseStatements(sql, p -> p.withDialect(Dialect.SQLSERVER)).size()); + } + } + + @Test + void preservesTheEntireProcedureBatch() throws Exception { + String sql = "ALTER PROCEDURE p AS BEGIN TRY BEGIN TRANSACTION; SELECT 1; COMMIT; END TRY; " + + "BEGIN CATCH ROLLBACK; END CATCH; SELECT 2;"; + CreateFunctionalStatement statement = parse(sql); + assertTrue(statement.formatDeclaration().endsWith("END CATCH; SELECT 2;")); + assertEquals(1, CCJSqlParserUtil.parseStatements(sql, p -> p.withDialect(Dialect.SQLSERVER)) + .size()); + } + + @Test + void keepsDefaultAndOtherDialectsUnchanged() throws Exception { + CreateFunction old = (CreateFunction) CCJSqlParserUtil + .parse("CREATE FUNCTION f() RETURNS @r TABLE (id int) AS BEGIN RETURN; END;"); + assertNull(old.getReturnType()); + String pg = "CREATE OR REPLACE FUNCTION f() RETURNS int AS $$ SELECT 1; $$ LANGUAGE sql;"; + assertTrue(CCJSqlParserUtil.parse(pg, p -> p.withDialect(Dialect.POSTGRESQL)).toString() + .startsWith("CREATE OR REPLACE FUNCTION")); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("CREATE OR ALTER FUNCTION f() RETURNS TABLE AS RETURN SELECT 1")); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE FUNCTION f() AS RETURN 1", + "ALTER FUNCTION f() RETURNS @r TABLE AS BEGIN RETURN; END", + "CREATE FUNCTION f() RETURNS @r TABLE () AS BEGIN RETURN; END", + "ALTER FUNCTION f() RETURNS int AS BEGIN RETURN 1;", + "ALTER FUNCTION f() RETURNS TABLE"}) + void rejectsIncompleteDeclarations(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void validatesOperationCapabilities() { + for (String prefix : List.of("CREATE", "ALTER", "CREATE OR ALTER")) { + assertTrue(new Validation( + CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.SQLSERVER) + .getConfiguration(), + List.of(SqlServerVersion.V2019), + prefix + " FUNCTION f() RETURNS TABLE AS RETURN SELECT 1").validate() + .isEmpty()); + } + assertFalse(new Validation( + CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.SQLSERVER) + .getConfiguration(), + List.of(new FeaturesAllowed(Feature.functionalStatement, Feature.createFunction)), + "ALTER FUNCTION f() RETURNS TABLE AS RETURN SELECT 1").validate().isEmpty()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/SqlServerSetOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/SqlServerSetOptionsTest.java new file mode 100644 index 0000000000..4b7b3f62fa --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/SqlServerSetOptionsTest.java @@ -0,0 +1,157 @@ +/*- + * #%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.statement; + +import java.util.Arrays; +import java.util.Collections; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.SetStatement.OnOffOption; +import net.sf.jsqlparser.statement.SetStatement.OnOffOptions; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SetStatementDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import net.sf.jsqlparser.util.validation.feature.SqlServerVersion; +import net.sf.jsqlparser.util.validation.validator.SetStatementValidator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class SqlServerSetOptionsTest { + private SetStatement parse(String sql) throws JSQLParserException { + return (SetStatement) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.SQLSERVER).withUnsupportedStatements(false)); + } + + private void assertRoundTrip(SetStatement set, String sql) throws Exception { + assertEquals(sql, set.toString()); + StringBuilder buffer = new StringBuilder(); + set.accept(new StatementDeParser(buffer), null); + assertEquals(sql, buffer.toString()); + assertEquals(sql, parse(buffer.toString()).toString()); + } + + @ParameterizedTest + @EnumSource(OnOffOption.class) + void parsesBothStatesAsOptions(OnOffOption option) throws Exception { + for (boolean on : new boolean[] {true, false}) { + String sql = "SET " + option + (on ? " ON" : " OFF"); + SetStatement set = parse(sql); + assertEquals(Collections.singletonList(option), set.getOnOffOptions().getOptions()); + assertEquals(on, set.getOnOffOptions().isOn()); + assertEquals(0, set.getCount()); + assertRoundTrip(set, sql); + } + } + + @Test + void retainsGroupedOptionsAndEditsTheirSharedValue() throws Exception { + SetStatement set = parse("set quoted_identifier, /* group */ ansi_nulls on"); + assertEquals(Arrays.asList(OnOffOption.QUOTED_IDENTIFIER, OnOffOption.ANSI_NULLS), + set.getOnOffOptions().getOptions()); + set.getOnOffOptions().getOptions().set(0, OnOffOption.NOCOUNT); + set.getOnOffOptions().setOn(false); + assertRoundTrip(set, "SET NOCOUNT, ANSI_NULLS OFF"); + assertTrue(new TablesNamesFinder().getTables(set).isEmpty()); + assertEquals(3, CCJSqlParserUtil.parseStatements( + "SET NOCOUNT ON; SET XACT_ABORT OFF; SELECT 1;", + p -> p.withDialect(Dialect.SQLSERVER)).size()); + Object context = new Object(); + assertSame(context, set.accept(new StatementVisitorAdapter() { + @Override + public Object visit(SetStatement statement, S value) { + assertSame(set, statement); + return value; + } + }, context)); + } + + @ParameterizedTest + @ValueSource(strings = {"SET NOCOUNT", "SET NOCOUNT TRUE", "SET NOCOUNT 1", + "SET NOCOUNT, ON", "SET NOCOUNT,, ANSI_NULLS ON", "SET NOCOUNT, unknown ON", + "SET NOCOUNT ON OFF", "SET NOCOUNT ON, ANSI_NULLS OFF"}) + void rejectsIncompleteOrMixedOptionGroups(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void retainsGenericAssignmentsAndDialectIsolation() throws Exception { + for (String sql : new String[] {"SET NOCOUNT = 1", "SET NOCOUNT.value = 1", + "SET v = 1, c = 3", "SET v = 1, 3", "SET @Flag = 1", + "SET LOCAL Time Zone 'UTC'", "SET standard_conforming_strings = on"}) { + SetStatement set = parse(sql); + assertNull(set.getOnOffOptions()); + assertEquals(CCJSqlParserUtil.parse(sql).toString(), set.toString()); + assertRoundTrip(set, set.toString()); + } + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse("SET NOCOUNT ON")); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse("SET NOCOUNT ON", + p -> p.withDialect(Dialect.POSTGRESQL))); + SetStatement generic = (SetStatement) CCJSqlParserUtil.parse("SET NOCOUNT OFF"); + assertNull(generic.getOnOffOptions()); + assertEquals(1, generic.getCount()); + } + + @Test + void sharesPunctuationWithoutBypassingCustomExpressionDeparsers() throws Exception { + SetStatement set = parse("SET v = 1, 3, c = 5"); + StringBuilder buffer = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 10); + } + }; + expressions.setBuilder(buffer); + new SetStatementDeParser(expressions, buffer).deParse(set); + assertEquals("SET v = 11, 13, c = 15", buffer.toString()); + } + + @Test + void switchesBetweenAssignmentAndOptionForms() throws Exception { + SetStatement set = parse("SET LOCAL x = 1"); + set.setOnOffOptions(new OnOffOptions(Collections.singleton(OnOffOption.NOCOUNT), true)); + assertNull(set.getEffectParameter()); + assertRoundTrip(set, "SET NOCOUNT ON"); + set.add("x", new ExpressionList<>(new LongValue(2)), true); + assertNull(set.getOnOffOptions()); + assertRoundTrip(set, "SET x = 2"); + set.setOnOffOptions(new OnOffOptions(Collections.singleton(OnOffOption.XACT_ABORT), false)); + set.clear(); + assertNull(set.getOnOffOptions()); + assertEquals(0, set.getCount()); + assertThrows(IllegalArgumentException.class, + () -> new OnOffOptions(Collections.emptyList(), true)); + } + + @Test + void validatesSqlServerCapabilitySeparatelyFromGenericSet() throws Exception { + SetStatement set = parse("SET ANSI_NULLS, QUOTED_IDENTIFIER ON"); + SetStatementValidator validator = new SetStatementValidator(); + validator.setContext(new ValidationContext().setCapabilities( + Arrays.asList(SqlServerVersion.V2019, PostgresqlVersion.V10))); + validator.validate(set); + assertFalse(validator.getValidationErrors().containsKey(SqlServerVersion.V2019)); + ValidationTestAsserts.assertNotSupported( + validator.getValidationErrors().get(PostgresqlVersion.V10), + Feature.sqlServerSetOptions); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureFallbackTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureFallbackTest.java new file mode 100644 index 0000000000..7826cddd85 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureFallbackTest.java @@ -0,0 +1,162 @@ +/*- + * #%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.statement; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.function.CreateFunction; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class StatementFeatureFallbackTest { + private static final class UnclassifiedStatement implements Statement { + @Override + public T accept(StatementVisitor visitor, S context) { + return null; + } + } + + private static void assertOpaque(StatementFeatures features, String... references) { + assertThat(features.getCertain()).containsExactly(StmtFeature.OPAQUE); + assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA, + StmtFeature.RETURNS_RESULT_SET, StmtFeature.MODIFIES_DATA, + StmtFeature.MODIFIES_SCHEMA); + assertThat(features.getUnresolvedReferences()).containsExactlyInAnyOrder(references); + } + + @Test + void unclassifiedStatementHasNoCatalogueReference() { + Statement statement = new UnclassifiedStatement(); + assertOpaque(statement.getFeatures()); + assertOpaque(statement.getFeatures(name -> true)); + assertOpaque(StatementFeatureVisitor.analyse(statement)); + } + + @Test + void anonymousStatementHasNoEmptyReference() { + Statement statement = new Statement() { + @Override + public T accept(StatementVisitor visitor, S context) { + return null; + } + }; + assertOpaque(statement.getFeatures()); + } + + @Test + void inertOracleStatementRetainsConservativeFallback() { + assertOpaque(new OracleNullStatement().getFeatures()); + assertOpaque(new OracleAssignment(new Column("x"), new LongValue(1)).getFeatures()); + } + + @Test + void fallbackPreservesFunctionReferenceFoundBeforeClassification() { + Function function = new Function(); + function.setName("Unproven_Function"); + Statement assignment = new OracleAssignment(new Column("x"), function); + assertOpaque(assignment.getFeatures(), "unproven_function"); + assertOpaque(assignment.getFeatures(name -> name.equals("unproven_function"))); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + void aggregateFallbackHasNoContainerReference(int count) { + Statements script = new Statements(); + for (int i = 0; i < count; i++) { + script.add(new UnclassifiedStatement()); + } + assertOpaque(StatementFeatureVisitor.analyse(script)); + assertOpaque(StatementFeatureVisitor.analyse(script, name -> true)); + List each = StatementFeatureVisitor.analyseEach(script); + assertThat(each).hasSize(count); + each.forEach(StatementFeatureFallbackTest::assertOpaque); + } + + @Test + void perStatementAnalysisDoesNotLeakReferencesOrResultPositions() throws Exception { + Statements script = new Statements(); + script.add(CCJSqlParserUtil.parse("CALL Do_Something(1)")); + script.add(new UnclassifiedStatement()); + script.add(CCJSqlParserUtil.parse("SELECT 1")); + List each = StatementFeatureVisitor.analyseEach(script); + assertOpaque(each.get(0), "do_something"); + assertOpaque(each.get(1)); + assertThat(each.get(2).getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + assertThat(each.get(2).getUncertain()).isEmpty(); + assertThat(each.get(2).getUnresolvedReferences()).isEmpty(); + StatementFeatures union = StatementFeatureVisitor.analyse(script); + assertThat(union.getCertain()).containsExactlyInAnyOrder(StmtFeature.OPAQUE, + StmtFeature.RETURNS_RESULT_SET); + assertThat(union.getUncertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA, + StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA); + assertThat(union.getUnresolvedReferences()).containsExactly("do_something"); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT 1", "CREATE TABLE t (id INTEGER)"}) + void classifiedStatementsDoNotFallBack(String sql) throws Exception { + StatementFeatures features = CCJSqlParserUtil.parse(sql).getFeatures(); + assertThat(features.getCertain()).containsExactly(sql.startsWith("SELECT") + ? StmtFeature.RETURNS_RESULT_SET + : StmtFeature.MODIFIES_SCHEMA); + assertThat(features.getUncertain()).isEmpty(); + assertThat(features.getUnresolvedReferences()).isEmpty(); + } + + @Test + void procedureAndDynamicSqlMarkersRemainResolvable() throws Exception { + assertOpaque(CCJSqlParserUtil.parse("CALL Do_Something(1)").getFeatures(), "do_something"); + assertOpaque(CCJSqlParserUtil.parse("DO 'BEGIN NULL; END'", + parser -> parser.withDialect(Dialect.POSTGRESQL)).getFeatures(), "do"); + assertOpaque(CCJSqlParserUtil.parse("some unsupported sql", + parser -> parser.withUnsupportedStatements(true)).getFeatures(), "unsupported"); + } + + @Test + void explainMarkerRemainsExplicit() throws Exception { + StatementFeatures features = CCJSqlParserUtil.parse("EXPLAIN SELECT 1").getFeatures(); + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA, + StmtFeature.READS_DATA); + assertThat(features.getUnresolvedReferences()).containsExactly("explain"); + } + + @Test + void selectFunctionReferenceAndAllowListRemainEffective() throws Exception { + Statement statement = CCJSqlParserUtil.parse("SELECT Unproven_Function(1)"); + StatementFeatures features = statement.getFeatures(); + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA, + StmtFeature.MODIFIES_SCHEMA); + assertThat(features.getUnresolvedReferences()).containsExactly("unproven_function"); + StatementFeatures pure = statement.getFeatures(name -> name.equals("unproven_function")); + assertThat(pure.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + assertThat(pure.getUncertain()).isEmpty(); + assertThat(pure.getUnresolvedReferences()).isEmpty(); + } + + @Test + void storedRoutineExplicitMarkerIsOutsideGenericFallback() { + StatementFeatures features = new CreateFunction().getFeatures(); + assertThat(features.getCertain()).containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA, + StmtFeature.READS_DATA); + assertThat(features.getUnresolvedReferences()).containsExactly("createfunction"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java new file mode 100644 index 0000000000..0e4f938e7d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java @@ -0,0 +1,143 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterRoutine; +import net.sf.jsqlparser.statement.alter.AlterStatistics; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterTrigger; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.user.CreateUser; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class StatementFeatureVisitorMetadataDdlTest { + static Stream definitions() { + return Stream.of( + Arguments.of("ALTER COLLATION c REFRESH VERSION", AlterCollation.class), + Arguments.of("ALTER COLLATION c RENAME TO c2", AlterCollation.class), + Arguments.of("ALTER DATABASE d READ ONLY = 0", AlterDatabase.class), + Arguments.of("ALTER DATABASE d CHARACTER SET utf8mb4 COLLATE utf8mb4_bin", + AlterDatabase.class), + Arguments.of("ALTER DATABASE d SET work_mem = '64MB'", AlterDatabase.class), + Arguments.of("ALTER DATABASE d OWNER TO CURRENT_USER", AlterDatabase.class), + Arguments.of("ALTER FUNCTION f(int) OWNER TO u1", AlterRoutine.class), + Arguments.of("ALTER PROCEDURE p SET search_path = public", AlterRoutine.class), + Arguments.of("ALTER FUNCTION f READS SQL DATA", AlterRoutine.class), + Arguments.of("ALTER TRIGGER t ON tbl RENAME TO t2", AlterTrigger.class), + Arguments.of("ALTER SCHEMA s RENAME TO s2", AlterSchema.class), + Arguments.of("ALTER SCHEMA s OWNER TO CURRENT_USER", AlterSchema.class), + Arguments.of("ALTER STATISTICS s RENAME TO s2", AlterStatistics.class), + Arguments.of("ALTER STATISTICS s SET STATISTICS 500", AlterStatistics.class), + Arguments.of("ALTER TABLE ALL IN TABLESPACE old_space SET TABLESPACE new_space", + AlterTablespaceMove.class), + Arguments.of( + "ALTER INDEX ALL IN TABLESPACE old_space SET TABLESPACE new_space NOWAIT", + AlterTablespaceMove.class), + Arguments.of("CREATE TEXT SEARCH CONFIGURATION c (COPY = pg_catalog.simple)", + CreateTextSearchConfiguration.class), + Arguments.of("CREATE TEXT SEARCH CONFIGURATION c (PARSER = p)", + CreateTextSearchConfiguration.class), + Arguments.of("ALTER TEXT SEARCH CONFIGURATION c ADD MAPPING FOR word WITH simple", + AlterTextSearchConfiguration.class), + Arguments.of("ALTER TEXT SEARCH CONFIGURATION c DROP MAPPING FOR word", + AlterTextSearchConfiguration.class), + Arguments.of("CREATE USER u", CreateUser.class), + Arguments.of("CREATE USER u IDENTIFIED BY 'password'", CreateUser.class)); + } + + @ParameterizedTest + @MethodSource("definitions") + void metadataDefinitionsHaveOnlySchemaEffectsAcrossEntryPoints(String sql, Class type) + throws Exception { + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + assertInstanceOf(type, statement); + assertSchemaOnly(statement.getFeatures()); + assertSchemaOnly(StatementFeatureVisitor.analyse(statement)); + assertSchemaOnly(StatementFeatureVisitor.analyse(sql)); + assertSchemaOnly(StatementFeatureVisitor.analyse(statement, name -> true)); + Statements script = CCJSqlParserUtil.parseStatements(sql); + assertSchemaOnly(StatementFeatureVisitor.analyse(script)); + List each = StatementFeatureVisitor.analyseEach(script); + assertEquals(1, each.size()); + assertSchemaOnly(each.get(0)); + } + + @ParameterizedTest + @MethodSource("definitions") + void classificationDoesNotSuppressFollowingExecutedSql(String sql, Class type) + throws Exception { + Statements script = CCJSqlParserUtil.parseStatements(sql + "; SELECT f(id) FROM t"); + assertInstanceOf(type, script.getStatements().get(0)); + List each = StatementFeatureVisitor.analyseEach(script); + assertSchemaOnly(each.get(0)); + assertEquals(Set.of("f"), each.get(1).getUnresolvedReferences()); + assertEquals(EnumSet.of(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET), + each.get(1).getCertain()); + StatementFeatures union = StatementFeatureVisitor.analyse(script); + assertEquals(EnumSet.of(StmtFeature.MODIFIES_SCHEMA, StmtFeature.READS_DATA, + StmtFeature.RETURNS_RESULT_SET), union.getCertain()); + assertEquals(Set.of("f"), union.getUnresolvedReferences()); + assertTrue(union.mayModifyData()); + } + + @ParameterizedTest + @ValueSource( + strings = {"CREATE ROLE r", "CREATE TRIGGER tr AFTER INSERT ON t EXECUTE FUNCTION f()", + "CREATE POLICY p ON t USING (f(id) > 0)"}) + void existingStoredDefinitionsRemainSchemaOnly(String sql) throws Exception { + assertSchemaOnly(TestUtils.assertSqlCanBeParsedAndDeparsed(sql).getFeatures()); + } + + @Test + void unknownAndUnsupportedStatementsRemainConservative() throws Exception { + Statement unknown = new Statement() { + @Override + public T accept(StatementVisitor visitor, S context) { + return null; + } + }; + assertOpaque(unknown.getFeatures()); + assertOpaque(StatementFeatureVisitor.analyse(new Statements())); + StatementFeatures unsupported = CCJSqlParserUtil + .parse("garbage", p -> p.withUnsupportedStatements(true)).getFeatures(); + assertOpaque(unsupported); + assertEquals(Set.of("unsupported"), unsupported.getUnresolvedReferences()); + } + + private static void assertSchemaOnly(StatementFeatures features) { + assertEquals(EnumSet.of(StmtFeature.MODIFIES_SCHEMA), features.getCertain()); + assertTrue(features.getUncertain().isEmpty()); + assertTrue(features.getUnresolvedReferences().isEmpty()); + } + + private static void assertOpaque(StatementFeatures features) { + assertEquals(EnumSet.of(StmtFeature.OPAQUE), features.getCertain()); + assertEquals( + EnumSet.of(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET, + StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA), + features.getUncertain()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorTest.java new file mode 100644 index 0000000000..b8b310994d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorTest.java @@ -0,0 +1,673 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +/*- + * Copyright (C) 2004 - 2026 JSQLParser + * Licensed under the Apache License, Version 2.0. + */ +package net.sf.jsqlparser.statement; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.SelectItem; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The interesting assertions here are the negative ones. Any naive implementation gets + * {@code SELECT} and {@code CREATE TABLE} right; what separates a correct analysis from a flag + * union is that it says no to a result set for {@code INSERT .. SELECT} and to a mutation + * for a pure read. + */ +class StatementFeatureVisitorTest { + + private static StatementFeatures analyse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql).getFeatures(); + } + + private static StatementFeatures analyse(String sql, String... pureFunctions) + throws JSQLParserException { + Set pure = Set.of(pureFunctions); + return StatementFeatureVisitor.analyse(CCJSqlParserUtil.parse(sql), pure::contains); + } + + // ---- invariants ------------------------------------------------------------------------- + + /** + *

What this pins down

+ * + * {@link StatementFeatures} splits its verdict into two sets: + * + *
    + *
  • {@code getCertain()} - the grammar proves the statement has this feature;
  • + *
  • {@code getUncertain()} - the grammar could not exclude it, but could not prove + * it either.
  • + *
+ * + * For the second set to be readable at all, it has to mean "unproven and nothing more". + * If a feature could appear in both sets, then {@code getUncertain()} would answer the question + * "is anything doubtful here?" with a yes even when the feature is settled, and printing it in + * a rejection message - which is exactly what the read-only guard in the README does - would + * report a proven mutation as a doubt. So the constructor normalises: + * {@code uncertain.removeAll(certain)}. This test is that normalisation's only guard. + * + *

Why the overlap is not hypothetical

+ * + * {@code UPDATE t SET a = f(b)} produces it on every run: + * + *
    + *
  • {@code visit(Update)} adds {@code MODIFIES_DATA} to certain;
  • + *
  • {@code FeatureExpressionVisitor#visit(Function)} cannot prove {@code f} pure, so it adds + * {@code MODIFIES_DATA} and {@code MODIFIES_SCHEMA} to uncertain.
  • + *
+ * + * Raw, that is {@code certain={MODIFIES_DATA}}, {@code uncertain={MODIFIES_DATA, + * MODIFIES_SCHEMA}}. Normalised, {@code uncertain} must come out as {@code {MODIFIES_SCHEMA}} + * alone - the schema change is the only open question; the data change is settled. + * + *

Why the previous version of this test was broken

+ * + * It looped over four statements asserting + * {@code assertThat(getCertain()).doesNotContainAnyElementsOf(getUncertain())}. Two problems: + * + *
    + *
  1. It threw. AssertJ rejects an empty argument to {@code doesNotContainAnyElementsOf} + * with "The iterable of values to look for should not be empty" - an + * {@code IllegalArgumentException} about the assertion's own inputs, not a failed expectation. + * {@code SELECT a FROM t} has nothing uncertain, so the first iteration blew up before testing + * anything.
  2. + *
  3. Three of the four inputs were dead weight. Only the {@code f(b)} case can ever + * produce an overlap; for the others the assertion held vacuously. A loop over inputs that + * cannot exercise the invariant hides the one input that can.
  4. + *
+ * + * Both are fixed below by intersecting explicitly (an empty intersection is a legitimate + * result, not a malformed assertion) and by testing the case that actually overlaps. + */ + @Test + @DisplayName("a proven feature is never also reported as merely possible") + void certainAndUncertainAreDisjoint() throws JSQLParserException { + StatementFeatures f = analyse("UPDATE t SET a = f(b)"); + + // the overlap the constructor has to resolve + assertThat(f.getCertain()).contains(StmtFeature.MODIFIES_DATA); + assertThat(f.getUncertain()) + .as("MODIFIES_DATA is proven, so it must not also be listed as doubtful") + .doesNotContain(StmtFeature.MODIFIES_DATA) + .as("the unproven function leaves the schema question genuinely open") + .contains(StmtFeature.MODIFIES_SCHEMA); + + assertDisjoint(f, "UPDATE t SET a = f(b)"); + } + + @Test + @DisplayName("the invariant holds for statements with nothing uncertain too") + void disjointWhenNothingIsUncertain() throws JSQLParserException { + for (String sql : new String[] {"SELECT a FROM t", "DELETE FROM t RETURNING *", + "CREATE TABLE t (a INT)", "UPDATE t SET a = f(b)"}) { + assertDisjoint(analyse(sql), sql); + } + } + + @Test + @DisplayName("normalisation happens in the constructor, not by luck of traversal order") + void constructorNormalisesOverlappingSets() { + // hand-built overlap, bypassing the visitor entirely: whatever order the visitor happens + // to add features in, the value object must still come out normalised + StatementFeatures f = new StatementFeatures( + EnumSet.of(StmtFeature.MODIFIES_DATA, StmtFeature.READS_DATA), + EnumSet.of(StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA), + Set.of("f")); + + assertThat(f.getCertain()) + .containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA, StmtFeature.READS_DATA); + assertThat(f.getUncertain()).containsExactly(StmtFeature.MODIFIES_SCHEMA); + + // and the two accessors stay consistent with the predicates + assertThat(f.is(StmtFeature.MODIFIES_DATA)).isTrue(); + assertThat(f.may(StmtFeature.MODIFIES_DATA)).isTrue(); + assertThat(f.is(StmtFeature.MODIFIES_SCHEMA)).isFalse(); + assertThat(f.may(StmtFeature.MODIFIES_SCHEMA)).isTrue(); + } + + /** + * Intersects the two sets and asserts the result is empty. + * + *

+ * Deliberately not {@code doesNotContainAnyElementsOf}: that assertion treats an empty argument + * as caller error and throws {@code IllegalArgumentException}, which turns the perfectly valid + * case "this statement has no uncertainty at all" into a test error. An explicit intersection + * has no such precondition - empty in, empty out, assertion passes. + */ + private static void assertDisjoint(StatementFeatures f, String description) { + EnumSet overlap = EnumSet.noneOf(StmtFeature.class); + overlap.addAll(f.getCertain()); + overlap.retainAll(f.getUncertain()); + + assertThat(overlap).as("features reported as both proven and doubtful for: %s", description) + .isEmpty(); + } + + @Test + @DisplayName("is() implies may(), never the reverse") + void certainImpliesPossible() throws JSQLParserException { + StatementFeatures f = analyse("DELETE FROM t WHERE a = f(1)"); + for (StmtFeature feature : StmtFeature.values()) { + assertThat(!f.is(feature) || f.may(feature)).as(feature.name()).isTrue(); + } + } + + @Test + @DisplayName("not cached: mutating the AST changes the verdict") + void notCached() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("DELETE FROM t WHERE a = 1"); + assertThat(statement.getFeatures().returnsResultSet()).isFalse(); + + ReturningClause returning = new ReturningClause(ReturningClause.Keyword.RETURNING, + List.of(new SelectItem<>(new Column("id")))); + ((Delete) statement).setReturningClause(returning); + + assertThat(statement.getFeatures().returnsResultSet()).isTrue(); + } + + // ---- plain cases ------------------------------------------------------------------------ + + @Test + void plainSelectReadsAndReturns() throws JSQLParserException { + StatementFeatures f = analyse("SELECT a FROM t"); + assertThat(f.returnsResultSet()).isTrue(); + assertThat(f.is(StmtFeature.READS_DATA)).isTrue(); + assertThat(f.mayModifyData()).isFalse(); + assertThat(f.modifiesSchema()).isFalse(); + } + + @Test + void insertValuesMutatesOnly() throws JSQLParserException { + StatementFeatures f = analyse("INSERT INTO t (a) VALUES (1)"); + assertThat(f.modifiesData()).isTrue(); + assertThat(f.returnsResultSet()).isFalse(); + } + + /** + * DDL that only touches the catalogue. No rows are read, written or returned. + */ + @Test + void nonDestructiveDdlTouchesTheSchemaOnly() throws JSQLParserException { + for (String sql : new String[] {"CREATE TABLE t (a INT)", "ALTER TABLE t ADD COLUMN b INT", + "GRANT SELECT ON t TO u", "CREATE INDEX i ON t (a)", "COMMENT ON TABLE t IS 'x'"}) { + StatementFeatures f = analyse(sql); + assertThat(f.modifiesSchema()).as(sql).isTrue(); + assertThat(f.returnsResultSet()).as(sql).isFalse(); + assertThat(f.mayModifyData()).as(sql).isFalse(); + } + } + + /** + * DROP and TRUNCATE are DDL by every other measure - implicit commit, no row triggers, + * catalogue change - but they destroy rows, so they carry {@code MODIFIES_DATA} as well. + * + *

+ * This is a deliberate call, not an oversight. The failure mode it prevents: + * + *

+     * if (connection.isReadOnly() && features.mayModifyData()) {
+     *     throw new SQLException("read-only");
+     * }
+     * 
+ * + * A guard written like that - and plenty are, because "modifies data" reads as the obvious + * check - would wave {@code DROP TABLE t} straight through if DROP were classified as schema + * only. Making the destructive DDL admit to destroying data means the naive guard is still + * correct. + * + *

+ * The cost is on the other side: a caller counting "did this write rows?" for auditing gets a + * yes for DROP, which is arguably wrong - no rows were written, they were removed along with + * the table. If that matters more to you than the guard case, move DROP (not TRUNCATE, which + * really is a bulk delete) back to schema-only and require guards to check both predicates. + */ + @Test + void destructiveDdlAlsoModifiesData() throws JSQLParserException { + for (String sql : new String[] {"DROP TABLE t", "TRUNCATE TABLE t"}) { + StatementFeatures f = analyse(sql); + assertThat(f.modifiesSchema()).as(sql).isTrue(); + assertThat(f.modifiesData()).as(sql).isTrue(); + assertThat(f.returnsResultSet()).as(sql).isFalse(); + } + } + + // ---- the cases a flag union gets wrong -------------------------------------------------- + + @Test + @DisplayName("INSERT .. SELECT contains a Select but returns no result set") + void insertSelectDoesNotReturnResultSet() throws JSQLParserException { + StatementFeatures f = analyse("INSERT INTO x (a) SELECT a FROM t"); + assertThat(f.modifiesData()).isTrue(); + assertThat(f.is(StmtFeature.READS_DATA)).isTrue(); + assertThat(f.returnsResultSet()).isFalse(); + assertThat(f.may(StmtFeature.RETURNS_RESULT_SET)).isFalse(); + } + + @Test + @DisplayName("data-modifying CTE: top-level SELECT returns rows, DELETE still mutates") + void dataModifyingCteAtTopLevel() throws JSQLParserException { + StatementFeatures f = + analyse("WITH c AS (DELETE FROM t RETURNING *) SELECT * FROM c"); + assertThat(f.returnsResultSet()).isTrue(); + assertThat(f.modifiesData()).isTrue(); + } + + @Test + @DisplayName("RETURNING inside a nested CTE of an INSERT returns nothing to the client") + void nestedReturningIsNotAResultSet() throws JSQLParserException { + StatementFeatures f = analyse( + "INSERT INTO x WITH c AS (DELETE FROM t RETURNING *) SELECT * FROM c"); + assertThat(f.modifiesData()).isTrue(); + assertThat(f.returnsResultSet()).isFalse(); + } + + @Test + void dmlWithReturningIsBoth() throws JSQLParserException { + StatementFeatures f = analyse("DELETE FROM t WHERE a = 1 RETURNING id, name"); + assertThat(f.modifiesData()).isTrue(); + assertThat(f.returnsResultSet()).isTrue(); + } + + // ---- pending: requires the expression / from-item traversal to be wired ----------------- + + @Nested + @DisplayName("subqueries and function purity") + class PendingExpressionWiring { + + @Test + void subqueryInWhereDoesNotAddAResultSet() throws JSQLParserException { + StatementFeatures f = analyse("UPDATE t SET a = 1 WHERE b IN (SELECT b FROM u)"); + assertThat(f.modifiesData()).isTrue(); + assertThat(f.is(StmtFeature.READS_DATA)).isTrue(); + assertThat(f.returnsResultSet()).isFalse(); + } + + @Test + void scalarSubqueryInSelectListStaysOneResultSet() throws JSQLParserException { + StatementFeatures f = analyse("SELECT (SELECT max(x) FROM u) AS m FROM t"); + assertThat(f.returnsResultSet()).isTrue(); + assertThat(f.mayModifyData()).isTrue(); // max() unproven + } + + @Test + void unprovenFunctionIsPossibleNotCertain() throws JSQLParserException { + StatementFeatures f = analyse("SELECT nextval('s')"); + assertThat(f.modifiesData()).isFalse(); + assertThat(f.mayModifyData()).isTrue(); + assertThat(f.getUnresolvedReferences()).contains("nextval"); + } + + @Test + void allowListCollapsesTheUncertainty() throws JSQLParserException { + StatementFeatures f = analyse("SELECT upper(name) FROM t", "upper"); + assertThat(f.mayModifyData()).isFalse(); + assertThat(f.getUnresolvedReferences()).isEmpty(); + } + } + + /** + * {@code ROWS FROM (..)} keeps its functions in {@code getRowsFromFunctions()} and leaves + * {@code getFunction()} null, so a check against the single function alone lets every one of + * them through as harmless. + */ + @Nested + @DisplayName("table functions in the FROM clause") + class TableFunctions { + + private StatementFeatures analyseNothingPure(String sql) throws JSQLParserException { + Predicate nothingIsPure = name -> false; + return StatementFeatureVisitor.analyse(CCJSqlParserUtil.parse(sql), nothingIsPure); + } + + @Test + void singleTableFunctionIsChecked() throws JSQLParserException { + String sql = "SELECT * FROM pg_ls_dir('.')"; + + StatementFeatures impure = analyseNothingPure(sql); + assertThat(impure.returnsResultSet()).isTrue(); + assertThat(impure.modifiesData()).isFalse(); + assertThat(impure.getUncertain()).contains(StmtFeature.MODIFIES_DATA, + StmtFeature.MODIFIES_SCHEMA); + assertThat(impure.getUnresolvedReferences()).containsExactly("pg_ls_dir"); + + StatementFeatures pure = analyse(sql, "pg_ls_dir"); + assertThat(pure.mayModifyData()).isFalse(); + assertThat(pure.getUnresolvedReferences()).isEmpty(); + } + + @Test + void rowsFromWithOneFunctionIsChecked() throws JSQLParserException { + String sql = "SELECT * FROM ROWS FROM (pg_ls_dir('.'))"; + + StatementFeatures impure = analyseNothingPure(sql); + assertThat(impure.returnsResultSet()).isTrue(); + assertThat(impure.modifiesData()).isFalse(); + assertThat(impure.getUncertain()).contains(StmtFeature.MODIFIES_DATA, + StmtFeature.MODIFIES_SCHEMA); + assertThat(impure.getUnresolvedReferences()).containsExactly("pg_ls_dir"); + + StatementFeatures pure = analyse(sql, "pg_ls_dir"); + assertThat(pure.mayModifyData()).isFalse(); + assertThat(pure.getUnresolvedReferences()).isEmpty(); + } + + @Test + void rowsFromChecksEveryFunction() throws JSQLParserException { + String sql = "SELECT * FROM ROWS FROM (generate_series(1, 2), pg_ls_dir('.'))"; + + StatementFeatures impure = analyseNothingPure(sql); + assertThat(impure.mayModifyData()).isTrue(); + assertThat(impure.getUnresolvedReferences()) + .containsExactlyInAnyOrder("generate_series", "pg_ls_dir"); + + StatementFeatures onlyFirstPure = analyse(sql, "generate_series"); + assertThat(onlyFirstPure.mayModifyData()) + .as("a pure first function must not hide the second") + .isTrue(); + assertThat(onlyFirstPure.getUnresolvedReferences()).containsExactly("pg_ls_dir"); + + StatementFeatures pure = analyse(sql, "generate_series", "pg_ls_dir"); + assertThat(pure.mayModifyData()).isFalse(); + assertThat(pure.getUnresolvedReferences()).isEmpty(); + } + + @Test + void rowsFromWithOrdinalityIsChecked() throws JSQLParserException { + String sql = "SELECT * FROM ROWS FROM (generate_series(1, 2), pg_ls_dir('.')) " + + "WITH ORDINALITY AS r(n, name, ord)"; + + StatementFeatures impure = analyseNothingPure(sql); + assertThat(impure.mayModifyData()).isTrue(); + assertThat(impure.getUnresolvedReferences()) + .containsExactlyInAnyOrder("generate_series", "pg_ls_dir"); + + StatementFeatures pure = analyse(sql, "generate_series", "pg_ls_dir"); + assertThat(pure.mayModifyData()).isFalse(); + assertThat(pure.getUnresolvedReferences()).isEmpty(); + } + } + + @Nested + @DisplayName("DDL and opaque statements") + class PendingStatementOverrides { + + @Test + void ctasIsSchemaAndReadButNotAResultSet() throws JSQLParserException { + StatementFeatures f = analyse("CREATE TABLE t AS SELECT a FROM u"); + assertThat(f.modifiesSchema()).isTrue(); + assertThat(f.is(StmtFeature.READS_DATA)).isTrue(); + assertThat(f.returnsResultSet()).isFalse(); + } + + @Test + void callIsOpaqueAndFailsSafeForGuardsOnly() throws JSQLParserException { + StatementFeatures f = analyse("CALL do_something(1)"); + assertThat(f.isOpaque()).isTrue(); + assertThat(f.mayModifyData()).isTrue(); + assertThat(f.modifiesData()).isFalse(); // guards block it, dispatchers are not misled + assertThat(f.getUnresolvedReferences()).isNotEmpty(); + } + } + + // ---- the failure mode that must stay loud ---------------------------------------------- + + @Test + @DisplayName("an unclassified statement type degrades to OPAQUE, never to harmless") + void unknownStatementTypeIsOpaque() { + Statement unknown = new Statement() { + @Override + public T accept(net.sf.jsqlparser.statement.StatementVisitor visitor, + S context) { + return null; + } + }; + StatementFeatures f = StatementFeatureVisitor.analyse(unknown, + Collections.emptySet()::contains); + assertThat(f.isOpaque()).isTrue(); + assertThat(f.mayModifyData()).isTrue(); + assertThat(f.modifiesData()).isFalse(); + } + + @Test + @DisplayName("purity predicate is consulted, not bypassed") + void purityPredicateIsUsed() throws JSQLParserException { + Predicate nothingIsPure = name -> false; + StatementFeatures f = StatementFeatureVisitor + .analyse(CCJSqlParserUtil.parse("SELECT a FROM t"), nothingIsPure); + assertThat(f.mayModifyData()).isFalse(); // no function call at all -> still clean + } + + // ---- cases the adapter wiring makes reachable ------------------------------------------- + + @Test + @DisplayName("data-modifying CTE inside a subquery does not blow up the select path") + void dataModifyingCteInsideASubquery() throws JSQLParserException { + // SelectVisitorAdapter#visit(WithItem) would ClassCastException here via getSelect() + StatementFeatures f = analyse( + "SELECT * FROM u WHERE id IN (" + + "WITH c AS (DELETE FROM t RETURNING id) SELECT id FROM c)"); + assertThat(f.returnsResultSet()).isTrue(); + assertThat(f.modifiesData()).isTrue(); + } + + @Test + @DisplayName("SELECT .. INTO materialises rows instead of returning them") + void selectIntoIsNotAResultSet() throws JSQLParserException { + StatementFeatures f = analyse("SELECT a INTO newtable FROM t"); + assertThat(f.returnsResultSet()).isFalse(); + assertThat(f.modifiesData()).isTrue(); + assertThat(f.modifiesSchema()).isTrue(); + } + + /** + *

Why the SQL looks the way it does

+ * + * {@code DELETE FROM t OUTPUT DELETED.id} does not parse. In {@code JSqlParserCC.jjt} + * the DELETE production only offers the OUTPUT clause inside the optional + * {@code (tables [OUTPUT ..] | )} group - i.e. the T-SQL + * {@code DELETE OUTPUT .. FROM } shape, where the target precedes OUTPUT and + * FROM follows it. Put OUTPUT after {@code FROM t} and there is no production for it. + * + *

+ * The strings below are lifted from the upstream {@code DeleteTest#testDeleteOutputClause}, so + * they are known to parse. + * + *

Why the INTO case uses a table variable

+ * + * The grammar is {@code ( UserVariable() | Table() )}. The {@code @MyTableVar} form + * populates {@code getTableVariable()} and leaves {@code getOutputTable()} null - which is + * exactly the case an implementation checking only {@code getOutputTable()} gets wrong. + */ + @Test + @DisplayName("OUTPUT returns rows; OUTPUT .. INTO redirects them") + void outputClauseDistinction() throws JSQLParserException { + StatementFeatures toClient = + analyse("DELETE Sales.ShoppingCartItem OUTPUT DELETED.* FROM Sales"); + assertThat(toClient.returnsResultSet()).isTrue(); + + StatementFeatures toTableVariable = analyse("DELETE Production.ProductProductPhoto " + + "OUTPUT DELETED.ProductID INTO @MyTableVar " + + "FROM Production.ProductProductPhoto AS ph"); + assertThat(toTableVariable.returnsResultSet()) + .as("rows go into @MyTableVar, not down the wire") + .isFalse(); + assertThat(toTableVariable.modifiesData()).isTrue(); + } + + @Test + @DisplayName("CREATE VIEW references tables without reading them") + void createViewDoesNotRead() throws JSQLParserException { + StatementFeatures f = analyse("CREATE VIEW v AS SELECT a FROM t"); + assertThat(f.modifiesSchema()).isTrue(); + assertThat(f.is(StmtFeature.READS_DATA)).isFalse(); + assertThat(f.returnsResultSet()).isFalse(); + } + + @Test + @DisplayName("script: the aggregate verdict is a union over all statements") + void scriptAggregateIsAUnion() throws JSQLParserException { + Statements script = + CCJSqlParserUtil.parseStatements("UPDATE t SET a = 1; SELECT a FROM t;"); + + StatementFeatures f = StatementFeatureVisitor.analyse(script); + + // useful for "may this script write?" - useless for executeQuery/executeUpdate, because + // the union does not say which statement returns the rows + assertThat(f.modifiesData()).isTrue(); + assertThat(f.returnsResultSet()).isTrue(); + } + + @Test + @DisplayName("script: each statement keeps its own result position") + void scriptStatementsAreIndependentlyTopLevel() throws JSQLParserException { + Statements script = + CCJSqlParserUtil.parseStatements("UPDATE t SET a = 1; SELECT a FROM t;"); + + List each = StatementFeatureVisitor.analyseEach(script); + + assertThat(each).hasSize(2); + + // the UPDATE writes and returns nothing + assertThat(each.get(0).modifiesData()).isTrue(); + assertThat(each.get(0).returnsResultSet()).isFalse(); + + // the SELECT returns rows and writes nothing - no leakage from the statement before it + assertThat(each.get(1).returnsResultSet()).isTrue(); + assertThat(each.get(1).mayModifyData()).isFalse(); + } + + @Test + @DisplayName("SELECT 1 reads nothing") + void selectWithoutFromReadsNothing() throws JSQLParserException { + StatementFeatures f = analyse("SELECT 1"); + assertThat(f.returnsResultSet()).isTrue(); + assertThat(f.is(StmtFeature.READS_DATA)).isFalse(); + } + + @Test + @DisplayName("SELECT .. FOR UPDATE takes locks") + void selectForUpdateTouchesTheTransaction() throws JSQLParserException { + StatementFeatures f = analyse("SELECT a FROM t FOR UPDATE"); + assertThat(f.is(StmtFeature.MODIFIES_TRANSACTION)).isTrue(); + } + + @Test + @DisplayName("EXPLAIN returns rows and may execute (EXPLAIN ANALYZE)") + void explainIsUncertain() throws JSQLParserException { + StatementFeatures f = analyse("EXPLAIN DELETE FROM t"); + assertThat(f.returnsResultSet()).isTrue(); + assertThat(f.mayModifyData()).isTrue(); + } + + /** + * Every expression position has to be traversed, otherwise a function call placed there escapes + * the purity check and a subquery placed there escapes the lock and write checks. + */ + @Nested + @DisplayName("every expression position is traversed") + class ExpressionPositions { + + private StatementFeatures analyseAllPureExcept(String sql, String impure) + throws JSQLParserException { + return StatementFeatureVisitor.analyse(CCJSqlParserUtil.parse(sql), + name -> !name.equals(impure)); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "SELECT substring(pg_read_file('x') FROM 2 FOR 3)", + "SELECT trim(LEADING 'x' FROM pg_read_file('x'))", + "SELECT position(pg_read_file('x') IN a) FROM t", + "SELECT overlay(a PLACING pg_read_file('x') FROM 2) FROM t", + "SELECT a FROM t WHERE a = ANY (SELECT pg_read_file('x'))", + "SELECT a FROM t WHERE a > ALL (SELECT pg_read_file('x'))", + "SELECT a FROM t WHERE a = SOME (SELECT pg_read_file('x'))", + "VALUES (pg_read_file('x'))", + "SELECT a FROM t WHERE a IN (VALUES (pg_read_file('x')))", + "SELECT a FROM t LIMIT length(pg_read_file('x'))", + "SELECT a FROM t WHERE a LIKE 'a' ESCAPE pg_read_file('x')", + "SELECT d AT TIME ZONE pg_read_file('x') FROM t", + "SELECT j -> pg_read_file('x') FROM t", + "SELECT json_objectagg(a : pg_read_file('x')) FROM t", + "SELECT a FROM t WHERE pg_read_file('x') MEMBER OF (j)", + "SELECT a FROM t START WITH a = pg_read_file('x') CONNECT BY PRIOR a = b", + "SELECT a FROM t LIMIT 1 BY pg_read_file('x')", + "WITH c AS (SELECT pg_read_file('x')) SELECT a FROM c UNION SELECT b FROM u", + "SELECT a FROM t UNION SELECT b FROM u ORDER BY pg_read_file('x')", + "SELECT a FROM t UNION SELECT b FROM u LIMIT length(pg_read_file('x'))", + "SELECT j ->> pg_read_file('x') FROM t", + "SELECT j #> ARRAY[pg_read_file('x')] FROM t", + "SELECT j #>> ARRAY[pg_read_file('x')] FROM t", + "SELECT a[length(pg_read_file('x'))] FROM t", + "SELECT t.a[length(pg_read_file('x'))] FROM t", + "SELECT a[1:length(pg_read_file('x'))] FROM t", + "SELECT a FROM t WHERE a[length(pg_read_file('x'))] = 1", + "SELECT 1 UNION SELECT 2 LIMIT length(pg_read_file('x'))", + "SELECT 1 UNION SELECT 2 FETCH FIRST length(pg_read_file('x')) ROWS ONLY"}) + void functionIsChecked(String sql) throws JSQLParserException { + StatementFeatures impure = analyseAllPureExcept(sql, "pg_read_file"); + assertThat(impure.getUnresolvedReferences()).containsExactly("pg_read_file"); + assertThat(impure.getUncertain()).contains(StmtFeature.MODIFIES_DATA, + StmtFeature.MODIFIES_SCHEMA); + + StatementFeatures pure = analyseAllPureExcept(sql, ""); + assertThat(pure.mayModifyData()).isFalse(); + assertThat(pure.getUnresolvedReferences()).isEmpty(); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "SELECT a FROM t WHERE a = ANY (SELECT b FROM u FOR UPDATE)", + "SELECT a FROM t WHERE a > ALL (SELECT b FROM u FOR UPDATE)", + "VALUES ((SELECT b FROM u FOR UPDATE))", + "SELECT a FROM t WHERE a IN (VALUES ((SELECT b FROM u FOR UPDATE)))", + "SELECT a FROM t LIMIT (SELECT b FROM u FOR UPDATE)"}) + void rowLockIsSeen(String sql) throws JSQLParserException { + StatementFeatures f = analyse(sql); + assertThat(f.is(StmtFeature.MODIFIES_TRANSACTION)).isTrue(); + assertThat(f.is(StmtFeature.READS_DATA)).isTrue(); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "SELECT a FROM t WHERE a = ANY (WITH c AS (DELETE FROM u RETURNING b) SELECT b FROM c)", + "VALUES ((WITH c AS (DELETE FROM u RETURNING b) SELECT b FROM c))"}) + void nestedWriteIsSeen(String sql) throws JSQLParserException { + StatementFeatures f = analyse(sql); + assertThat(f.modifiesData()).isTrue(); + assertThat(f.returnsResultSet()).isTrue(); + } + + @Test + void topLevelValuesReturnsRowsWithoutReading() throws JSQLParserException { + StatementFeatures f = analyse("VALUES (1, 2)"); + assertThat(f.returnsResultSet()).isTrue(); + assertThat(f.is(StmtFeature.READS_DATA)).isFalse(); + assertThat(f.mayModifyData()).isFalse(); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementsTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementsTest.java index 835b9ca4f3..5708b83b4d 100644 --- a/src/test/java/net/sf/jsqlparser/statement/StatementsTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/StatementsTest.java @@ -9,16 +9,17 @@ */ package net.sf.jsqlparser.statement; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParser; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.parser.ParseException; import net.sf.jsqlparser.parser.StringProvider; import net.sf.jsqlparser.statement.select.Select; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNull; - import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -93,7 +94,7 @@ public void testStatementsErrorRecovery3() throws JSQLParserException, ParseExce @Test public void testStatementsErrorRecovery4() throws JSQLParserException { Statements statements = CCJSqlParserUtil.parseStatements( - "select * from mytable; select from; select * from mytable2; select 4 from dual;", + "select * from mytable; shutdown defrag; select * from mytable2; select 4 from dual;", parser -> parser.withUnsupportedStatements()); assertEquals(4, statements.size()); @@ -103,6 +104,14 @@ public void testStatementsErrorRecovery4() throws JSQLParserException { assertInstanceOf(Select.class, statements.get(2)); assertInstanceOf(Select.class, statements.get(3)); - TestUtils.assertStatementCanBeDeparsedAs(statements.get(1), "select from", true); + TestUtils.assertStatementCanBeDeparsedAs(statements.get(1), "shutdown defrag", true); } + + @Test + void malformedKnownStatementDoesNotUseUnsupportedCapture() { + assertThrowsExactly(JSQLParserException.class, () -> CCJSqlParserUtil.parseStatements( + "select * from mytable; select from; select * from mytable2; select 4 from dual;", + parser -> parser.withUnsupportedStatements())); + } + } diff --git a/src/test/java/net/sf/jsqlparser/statement/TableMaintenanceStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/TableMaintenanceStatementTest.java new file mode 100644 index 0000000000..0f7ad33af4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/TableMaintenanceStatementTest.java @@ -0,0 +1,216 @@ +/*- + * #%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.statement; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.TableMaintenanceStatement.Modifier; +import net.sf.jsqlparser.statement.TableMaintenanceStatement.Operation; +import net.sf.jsqlparser.statement.TableMaintenanceStatement.Option; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TableMaintenanceStatementTest { + + private static TableMaintenanceStatement parse(String sql) throws JSQLParserException { + return (TableMaintenanceStatement) assertSqlCanBeParsedAndDeparsed(sql, true); + } + + private static List tableNames(TableMaintenanceStatement statement) { + return statement.getTables().stream().map(Table::getFullyQualifiedName) + .collect(Collectors.toList()); + } + + @Test + void optimizeTable() throws JSQLParserException { + TableMaintenanceStatement statement = parse("OPTIMIZE TABLE t"); + + assertEquals(Operation.OPTIMIZE, statement.getOperation()); + assertNull(statement.getModifier()); + assertFalse(statement.isTablesKeyword()); + assertEquals(List.of("t"), tableNames(statement)); + assertTrue(statement.getOptions().isEmpty()); + } + + @Test + void optimizeModifierAndTableList() throws JSQLParserException { + TableMaintenanceStatement statement = + parse("OPTIMIZE NO_WRITE_TO_BINLOG TABLE db.t1, `t 2`"); + + assertEquals(Modifier.NO_WRITE_TO_BINLOG, statement.getModifier()); + assertEquals(List.of("db.t1", "`t 2`"), tableNames(statement)); + } + + @Test + void localModifierAndTablesKeyword() throws JSQLParserException { + TableMaintenanceStatement statement = parse("OPTIMIZE LOCAL TABLES t1, t2"); + + assertEquals(Modifier.LOCAL, statement.getModifier()); + assertTrue(statement.isTablesKeyword()); + assertEquals("OPTIMIZE LOCAL TABLES t1, t2", statement.toString()); + } + + @Test + void repairOptions() throws JSQLParserException { + TableMaintenanceStatement statement = + parse("REPAIR NO_WRITE_TO_BINLOG TABLE t QUICK EXTENDED USE_FRM"); + + assertEquals(Operation.REPAIR, statement.getOperation()); + assertEquals(List.of(Option.QUICK, Option.EXTENDED, Option.USE_FRM), + statement.getOptions()); + } + + @Test + void checkOptionsKeepSourceOrder() throws JSQLParserException { + TableMaintenanceStatement statement = + parse("CHECK TABLE t1, t2 FOR UPGRADE QUICK FAST MEDIUM EXTENDED CHANGED"); + + assertEquals(Operation.CHECK, statement.getOperation()); + assertEquals(List.of(Option.FOR_UPGRADE, Option.QUICK, Option.FAST, Option.MEDIUM, + Option.EXTENDED, Option.CHANGED), statement.getOptions()); + assertEquals(List.of("t1", "t2"), tableNames(statement)); + } + + @Test + void checksumTableWithOption() throws JSQLParserException { + TableMaintenanceStatement statement = parse("CHECKSUM TABLE t1, db.t2 QUICK"); + + assertEquals(Operation.CHECKSUM, statement.getOperation()); + assertNull(statement.getModifier()); + assertEquals(List.of("t1", "db.t2"), tableNames(statement)); + assertEquals(List.of(Option.QUICK), statement.getOptions()); + } + + @ParameterizedTest + @ValueSource(strings = {"CHECKSUM TABLE t", "CHECKSUM TABLE t EXTENDED", + "CHECKSUM TABLES t1, t2"}) + void checksumTableForms(String sql) throws JSQLParserException { + assertEquals(Operation.CHECKSUM, parse(sql).getOperation()); + for (Dialect dialect : List.of(Dialect.MYSQL, Dialect.MARIADB)) { + assertEquals(sql, CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)) + .toString()); + } + } + + @Test + void keywordsAreCaseInsensitive() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("check tables t medium for upgrade"); + + assertEquals("CHECK TABLES t MEDIUM FOR UPGRADE", statement.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CHECK NO_WRITE_TO_BINLOG TABLE t", + "CHECK LOCAL TABLE t", + "CHECK TABLE t USE_FRM", + "OPTIMIZE TABLE t QUICK", + "REPAIR TABLE t FAST", + "REPAIR TABLE t FOR UPGRADE", + "OPTIMIZE t", + "REPAIR TABLE", + "CHECKSUM NO_WRITE_TO_BINLOG TABLE t", + "CHECKSUM LOCAL TABLE t", + "CHECKSUM TABLE t FAST", + "CHECKSUM TABLE t FOR UPGRADE", + "CHECKSUM TABLE t QUICK EXTENDED", + "CHECKSUM t" + }) + void rejectsSyntaxOutsideTheStatementDefinition(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @Test + void parsesInMySqlAndMariaDbDialects() throws JSQLParserException { + for (Dialect dialect : List.of(Dialect.MYSQL, Dialect.MARIADB)) { + Statement statement = CCJSqlParserUtil.parse("OPTIMIZE TABLE t", + parser -> parser.withDialect(dialect)); + assertInstanceOf(TableMaintenanceStatement.class, statement, dialect.name()); + } + } + + @Test + void buildsAndDeparses() { + TableMaintenanceStatement statement = new TableMaintenanceStatement(Operation.REPAIR) + .withModifier(Modifier.LOCAL).addTables(new Table("t1"), new Table("t2")) + .addOptions(Option.QUICK); + + assertDeparse(statement, "REPAIR LOCAL TABLE t1, t2 QUICK"); + } + + @Test + void findsTables() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("CHECK TABLE a, s.b QUICK"); + + assertThat(TablesNamesFinder.findTables(statement.toString())) + .containsExactlyInAnyOrder("a", "s.b"); + } + + @Test + void wordsRemainUsableOutsideTheStatement() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT optimize, repair, quick, fast, medium, changed " + + "FROM maintenance"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t OPTIMIZE PARTITION p1"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t CHECK PARTITION p1"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t REPAIR PARTITION p1"); + assertSqlCanBeParsedAndDeparsed( + "SELECT checksum, CHECKSUM(*), CHECKSUM(a, b) FROM checksum"); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (checksum INT) CHECKSUM = 1"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t CHECKSUM = 0, ADD COLUMN checksum INT"); + } + + @Test + void domainCheckConstraintIsNotATableCheck() throws JSQLParserException { + Statements statements = CCJSqlParserUtil.parseStatements( + "CREATE DOMAIN d AS INT CHECK (VALUE > 0);\nCHECK TABLE t;", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + + assertEquals(2, statements.size()); + assertEquals("CREATE DOMAIN d AS INT CHECK (VALUE > 0)", + statements.get(0).toString()); + assertInstanceOf(TableMaintenanceStatement.class, statements.get(1)); + } + + @Test + void reportsStatementFeatures() throws JSQLParserException { + StatementFeatures check = CCJSqlParserUtil.parse("CHECK TABLE t").getFeatures(); + assertThat(check.getCertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA, + StmtFeature.RETURNS_RESULT_SET); + + StatementFeatures checksum = CCJSqlParserUtil.parse("CHECKSUM TABLE t").getFeatures(); + assertThat(checksum.getCertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA, + StmtFeature.RETURNS_RESULT_SET); + assertFalse(checksum.may(StmtFeature.MODIFIES_SCHEMA)); + + StatementFeatures optimize = CCJSqlParserUtil.parse("OPTIMIZE TABLE t").getFeatures(); + assertThat(optimize.getCertain()).contains(StmtFeature.MODIFIES_SCHEMA); + assertFalse(optimize.may(StmtFeature.MODIFIES_DATA)); + + StatementFeatures repair = CCJSqlParserUtil.parse("REPAIR TABLE t").getFeatures(); + assertThat(repair.getCertain()).contains(StmtFeature.MODIFIES_SCHEMA); + assertThat(repair.getUncertain()).contains(StmtFeature.MODIFIES_DATA); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/TransactionStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/TransactionStatementTest.java new file mode 100644 index 0000000000..e05764ea47 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/TransactionStatementTest.java @@ -0,0 +1,312 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.StringReader; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.StringProvider; +import net.sf.jsqlparser.parser.StreamProvider; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TransactionStatementTest { + private Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + } + + private Statement roundTrip(String sql, Dialect dialect) throws Exception { + Statement statement = parse(sql, dialect); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + Statement reparsed = parse(output.toString(), dialect); + assertEquals(statement.getClass(), reparsed.getClass()); + assertEquals(statement.toString(), reparsed.toString()); + assertEquals(Set.of(StmtFeature.MODIFIES_TRANSACTION), + statement.getFeatures().getCertain()); + assertTrue(statement.getFeatures().getUncertain().isEmpty()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + return statement; + } + + @ParameterizedTest + @ValueSource(strings = {"START TRANSACTION", "BEGIN", "BEGIN WORK", "BEGIN TRANSACTION", + "START TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE", + "BEGIN ISOLATION LEVEL REPEATABLE READ READ WRITE NOT DEFERRABLE", + "BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED", + "START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED", + "BEGIN NOT DEFERRABLE, READ ONLY, ISOLATION LEVEL SERIALIZABLE"}) + void postgresStartsAreStructured(String sql) throws Exception { + StartTransaction statement = assertInstanceOf(StartTransaction.class, + roundTrip(sql, Dialect.POSTGRESQL)); + assertEquals(sql.startsWith("BEGIN") ? StartTransaction.Command.BEGIN + : StartTransaction.Command.START_TRANSACTION, statement.getCommand()); + assertEquals(sql.startsWith("BEGIN WORK") ? TransactionKeyword.WORK + : sql.startsWith("BEGIN TRANSACTION") ? TransactionKeyword.TRANSACTION : null, + statement.getKeyword()); + } + + @ParameterizedTest + @ValueSource(strings = {"START TRANSACTION", "BEGIN", "BEGIN WORK", + "START TRANSACTION READ ONLY", "START TRANSACTION READ WRITE", + "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY", + "START TRANSACTION READ WRITE, WITH CONSISTENT SNAPSHOT"}) + void mysqlStartsAreStructured(String sql) throws Exception { + assertInstanceOf(StartTransaction.class, roundTrip(sql, Dialect.MYSQL)); + } + + @ParameterizedTest + @ValueSource(strings = {"START TRANSACTION READ ONLY, READ WRITE", + "START TRANSACTION READ WRITE, READ ONLY"}) + void contradictoryAccessModesAreRejectedOnlyForMysql(String sql) throws Exception { + JSQLParserException error = assertThrows(JSQLParserException.class, + () -> parse(sql, Dialect.MYSQL)); + assertTrue(error.getCause().getMessage() + .contains("cannot specify both READ ONLY and READ WRITE")); + assertThrows(JSQLParserException.class, + () -> parse(sql.replace(", ", ", WITH CONSISTENT SNAPSHOT, "), Dialect.MYSQL)); + StartTransaction postgres = assertInstanceOf(StartTransaction.class, + roundTrip(sql, Dialect.POSTGRESQL)); + List expected = sql.endsWith("READ WRITE") + ? List.of(StartTransaction.Mode.READ_ONLY, StartTransaction.Mode.READ_WRITE) + : List.of(StartTransaction.Mode.READ_WRITE, StartTransaction.Mode.READ_ONLY); + assertEquals(expected, postgres.getModes()); + assertEquals(sql, postgres.toString()); + StartTransaction permissive = assertInstanceOf(StartTransaction.class, + CCJSqlParserUtil.parse(sql)); + assertEquals(expected, permissive.getModes()); + assertEquals(sql, permissive.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"START TRANSACTION READ ONLY, READ ONLY", + "START TRANSACTION READ WRITE, READ WRITE"}) + void mysqlRepeatedAccessModesArePreserved(String sql) throws Exception { + StartTransaction statement = assertInstanceOf(StartTransaction.class, + roundTrip(sql, Dialect.MYSQL)); + StartTransaction.Mode mode = sql.endsWith("READ ONLY") + ? StartTransaction.Mode.READ_ONLY + : StartTransaction.Mode.READ_WRITE; + assertEquals(List.of(mode, mode), statement.getModes()); + assertEquals(sql, statement.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"COMMIT", "COMMIT WORK", "COMMIT TRANSACTION", "COMMIT AND CHAIN", + "COMMIT WORK AND NO CHAIN", "COMMIT TRANSACTION AND CHAIN", "ROLLBACK", + "ROLLBACK WORK", "ROLLBACK TRANSACTION", "ROLLBACK AND CHAIN", + "ROLLBACK WORK AND NO CHAIN", "ROLLBACK TRANSACTION AND CHAIN"}) + void postgresCompletionRetainsKeywordAndChain(String sql) throws Exception { + Statement statement = roundTrip(sql, Dialect.POSTGRESQL); + TransactionChain chain = sql.endsWith("NO CHAIN") ? TransactionChain.NO_CHAIN + : sql.endsWith("CHAIN") ? TransactionChain.CHAIN : null; + TransactionKeyword keyword = sql.contains("WORK") ? TransactionKeyword.WORK + : sql.contains("TRANSACTION") ? TransactionKeyword.TRANSACTION : null; + if (statement instanceof Commit) { + assertEquals(chain, ((Commit) statement).getChain()); + assertEquals(keyword, ((Commit) statement).getKeyword()); + } else { + RollbackStatement rollback = assertInstanceOf(RollbackStatement.class, statement); + assertEquals(chain, rollback.getChain()); + assertEquals(keyword, rollback.getKeyword()); + assertEquals(keyword == TransactionKeyword.WORK, rollback.isUsingWorkKeyword()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"COMMIT WORK AND CHAIN", "COMMIT AND NO CHAIN", + "ROLLBACK WORK AND NO CHAIN", "ROLLBACK AND CHAIN"}) + void mysqlCompletionPreservesChain(String sql) throws Exception { + roundTrip(sql, Dialect.MYSQL); + } + + @ParameterizedTest + @ValueSource(strings = {"SAVEPOINT \"a b\"", "ROLLBACK TO \"a b\"", + "ROLLBACK TRANSACTION TO SAVEPOINT \"a b\"", "RELEASE SAVEPOINT \"a b\"", + "RELEASE \"a b\"", "RELEASE sp"}) + void postgresSavepointNamesAndOptionalKeyword(String sql) throws Exception { + roundTrip(sql, Dialect.POSTGRESQL); + } + + @Test + void mysqlSavepointNamesAreQuotedAndMutable() throws Exception { + roundTrip("SAVEPOINT `a b`", Dialect.MYSQL); + roundTrip("ROLLBACK TO SAVEPOINT `a b`", Dialect.MYSQL); + ReleaseSavepointStatement statement = assertInstanceOf(ReleaseSavepointStatement.class, + roundTrip("RELEASE SAVEPOINT `a b`", Dialect.MYSQL)); + assertEquals("`a b`", statement.getSavepointName()); + assertTrue(statement.isUsingSavepointKeyword()); + statement.withSavepointName("changed").withUsingSavepointKeyword(false); + assertEquals("RELEASE changed", statement.toString()); + assertEquals("RELEASE changed", statement.appendTo(new StringBuilder()).toString()); + } + + @Test + void typedModesCanBeChangedWithoutKeepingAnOpaqueSource() throws Exception { + StartTransaction statement = assertInstanceOf(StartTransaction.class, parse( + "BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY NOT DEFERRABLE", + Dialect.POSTGRESQL)); + assertEquals(List.of(StartTransaction.Mode.ISOLATION_LEVEL_REPEATABLE_READ, + StartTransaction.Mode.READ_ONLY, StartTransaction.Mode.NOT_DEFERRABLE), + statement.getModes()); + statement.withCommand(StartTransaction.Command.START_TRANSACTION).withKeyword(null); + statement.getModes().set(1, StartTransaction.Mode.READ_WRITE); + statement.addMode(StartTransaction.Mode.DEFERRABLE); + String sql = + "START TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, NOT DEFERRABLE, DEFERRABLE"; + assertEquals(sql, statement.toString()); + assertEquals(sql, roundTrip(sql, Dialect.POSTGRESQL).toString()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(sql, output.toString()); + } + + @Test + void legacyRollbackMutatorsStillControlWorkAndSavepoint() throws Exception { + RollbackStatement rollback = new RollbackStatement().withUsingWorkKeyword(true) + .withUsingSavepointKeyword(true).withSavepointName("sp"); + assertEquals("ROLLBACK WORK TO SAVEPOINT sp", rollback.toString()); + rollback.setUsingWorkKeyword(false); + assertNull(rollback.getKeyword()); + rollback.setKeyword(TransactionKeyword.TRANSACTION); + assertFalse(rollback.isUsingWorkKeyword()); + rollback.setUsingWorkKeyword(true); + assertEquals(TransactionKeyword.WORK, rollback.getKeyword()); + roundTrip("ROLLBACK WORK FORCE '25.32.87'", Dialect.ORACLE); + rollback.withSavepointName(null).withChain(TransactionChain.NO_CHAIN); + assertEquals("ROLLBACK WORK AND NO CHAIN", rollback.toString()); + Commit commit = new Commit().withKeyword(TransactionKeyword.WORK) + .withChain(TransactionChain.CHAIN); + assertEquals("COMMIT WORK AND CHAIN", commit.toString()); + } + + @Test + void statementBoundariesAndProceduralBlocksRemainSeparate() throws Exception { + for (Dialect dialect : List.of(Dialect.POSTGRESQL, Dialect.MYSQL)) { + Statements script = CCJSqlParserUtil.parseStatements( + "BEGIN; SELECT 1; SAVEPOINT sp; ROLLBACK TO SAVEPOINT sp; RELEASE SAVEPOINT sp; COMMIT AND NO CHAIN; SELECT 2", + p -> p.withDialect(dialect)); + assertEquals(7, script.size()); + assertInstanceOf(StartTransaction.class, script.get(0)); + assertInstanceOf(ReleaseSavepointStatement.class, script.get(4)); + assertInstanceOf(Commit.class, script.get(5)); + assertEquals("SELECT 2", script.get(6).toString()); + assertEquals(7, CCJSqlParserUtil.parseStatements(script.toString(), + p -> p.withDialect(dialect)).size()); + assertInstanceOf(Block.class, parse("BEGIN SELECT 1; END", dialect)); + } + assertInstanceOf(Block.class, CCJSqlParserUtil.parse("BEGIN SELECT 1; END")); + assertInstanceOf(OracleBlock.class, parse("BEGIN NULL; END;", Dialect.ORACLE)); + assertInstanceOf(StartTransaction.class, CCJSqlParserUtil.parse("START TRANSACTION")); + assertInstanceOf(ReleaseSavepointStatement.class, CCJSqlParserUtil.parse("RELEASE sp")); + } + + @Test + void directAndReaderEntryPointsUseTheSameDialectDisambiguation() throws Exception { + assertInstanceOf(StartTransaction.class, new CCJSqlParser(new StringProvider("BEGIN")) + .withDialect(Dialect.POSTGRESQL).Statement()); + assertInstanceOf(StartTransaction.class, + new CCJSqlParser(new StreamProvider(new StringReader("BEGIN"))) + .withDialect(Dialect.MYSQL).Statement()); + assertInstanceOf(StartTransaction.class, CCJSqlParserUtil.newParser("BEGIN") + .withDialect(Dialect.POSTGRESQL).Statement()); + } + + @Test + void releaseRemainsAvailableAsAnIdentifier() throws Exception { + for (String sql : List.of("SELECT release FROM release", "SELECT release(1) AS release", + "CREATE TABLE release (release INT)", "SAVEPOINT release")) { + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + } + } + + @Test + void visitorDispatchPassesContextAndResult() throws Exception { + StatementVisitorAdapter visitor = new StatementVisitorAdapter<>() { + @Override + public String visit(StartTransaction statement, S context) { + return context + ":" + statement.getCommand(); + } + + @Override + public String visit(ReleaseSavepointStatement statement, S context) { + return context + ":" + statement.getSavepointName(); + } + }; + assertEquals("ctx:BEGIN", parse("BEGIN", Dialect.POSTGRESQL).accept(visitor, "ctx")); + assertEquals("ctx:sp", parse("RELEASE sp", Dialect.POSTGRESQL).accept(visitor, "ctx")); + assertNull( + parse("BEGIN", Dialect.POSTGRESQL).accept(new StatementVisitorAdapter<>(), null)); + } + + @ParameterizedTest + @ValueSource(strings = {"START", "START TRANSACTION READ", + "START TRANSACTION ISOLATION LEVEL BAD", + "BEGIN ISOLATION READ COMMITTED", "BEGIN READ ONLY,", "BEGIN , READ ONLY", + "COMMIT AND", "COMMIT AND NO", "COMMIT AND CHAIN AND CHAIN", + "ROLLBACK AND", "ROLLBACK TO SAVEPOINT sp AND CHAIN", "ROLLBACK AND CHAIN TO sp", + "RELEASE", "RELEASE SAVEPOINT", "RELEASE SAVEPOINT sp garbage", + "COMMIT PREPARED 'id'", "ROLLBACK PREPARED 'id'", "XA START 'id'", + "START TRANSACTION WITH CONSISTENT SNAPSHOT"}) + void malformedAndDeferredPostgresSyntaxIsRejected(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @ValueSource(strings = {"BEGIN TRANSACTION", "BEGIN READ ONLY", "START TRANSACTION DEFERRABLE", + "START TRANSACTION ISOLATION LEVEL READ COMMITTED", "RELEASE sp", + "START TRANSACTION READ ONLY WITH CONSISTENT SNAPSHOT", "COMMIT TRANSACTION", + "ROLLBACK TRANSACTION"}) + void explicitMysqlDialectRejectsPostgresOnlySyntax(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL)); + } + + @Test + void validatesNewCapabilitiesAndPostgresChainVersion() { + FeatureConfiguration pg = new FeatureConfiguration().setValue(Feature.dialect, + Dialect.POSTGRESQL.name()); + assertTrue(new Validation(pg, List.of(PostgresqlVersion.V10), + "BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE; RELEASE sp") + .validate().isEmpty()); + assertFalse(new Validation(pg, List.of(PostgresqlVersion.V11), "COMMIT AND CHAIN") + .validate().isEmpty()); + assertTrue(new Validation(pg, List.of(PostgresqlVersion.V12), + "COMMIT AND CHAIN; ROLLBACK AND NO CHAIN").validate().isEmpty()); + assertFalse(new Validation(pg, List.of(new FeaturesAllowed(Feature.commit)), + "COMMIT AND CHAIN").validate().isEmpty()); + assertFalse(new Validation(pg, List.of(new FeaturesAllowed(Feature.select)), + "BEGIN; RELEASE sp").validate().isEmpty()); + FeatureConfiguration mysql = new FeatureConfiguration().setValue(Feature.dialect, + Dialect.MYSQL.name()); + assertTrue(new Validation(mysql, List.of(MySqlVersion.V8_0), + "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY; RELEASE SAVEPOINT sp; COMMIT AND CHAIN") + .validate().isEmpty()); + assertFalse(new Validation(pg, List.of(MySqlVersion.V8_0), + "START TRANSACTION ISOLATION LEVEL SERIALIZABLE DEFERRABLE") + .validate().isEmpty()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/UnsupportedIfBoundaryTest.java b/src/test/java/net/sf/jsqlparser/statement/UnsupportedIfBoundaryTest.java new file mode 100644 index 0000000000..a9952e140e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/UnsupportedIfBoundaryTest.java @@ -0,0 +1,54 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.Select; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class UnsupportedIfBoundaryTest { + @ParameterizedTest + @ValueSource(strings = {"IF x > 0 SELECT 1 WHERE", "IF x > 0 SELECT * FROM"}) + void malformedIfBodyCannotBecomeAnOpaqueSuffix(String sql) { + for (boolean unsupported : new boolean[] {false, true}) { + for (String prefix : new String[] {"", "SELECT 0;"}) { + assertThrowsExactly(JSQLParserException.class, + () -> CCJSqlParserUtil.parseStatements(prefix + sql + ";SELECT 2;", + p -> p.withUnsupportedStatements(unsupported))); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"IF x > 0 SELECT 1 WHERE", "IF x > 0 SELECT * FROM"}) + void malformedIfBodyRecordsOneErrorWithoutPublishingPartialIf(String sql) throws Exception { + for (boolean unsupported : new boolean[] {false, true}) { + for (String prefix : new String[] {"", "SELECT 0;"}) { + CCJSqlParser parser = CCJSqlParserUtil.newParser(prefix + sql + ";SELECT 2;") + .withUnsupportedStatements(unsupported).withErrorRecovery(true); + Statements statements = parser.Statements(); + int index = prefix.isEmpty() ? 0 : 1; + assertEquals(index + 2, statements.size()); + assertNull(statements.get(index)); + assertEquals("SELECT 2", + assertInstanceOf(Select.class, statements.get(index + 1)).toString()); + assertEquals(1, parser.getParseErrors().size()); + } + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementRecoveryTest.java b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementRecoveryTest.java new file mode 100644 index 0000000000..d3d5ade826 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementRecoveryTest.java @@ -0,0 +1,296 @@ +/*- + * #%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.statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.ParseException; +import net.sf.jsqlparser.parser.StreamProvider; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class UnsupportedStatementRecoveryTest { + static Stream malformedStatements() { + return Stream.of("SELECT * FROM", "INSERT INTO t (a", "UPDATE t SET", + "SELECT", "INSERT", "UPDATE", "WITH", "SELECT 1 +", + "insert into t values (1,", "select f(1, 2) from t where x =", + "IF x > 0 UPDATE t SET", "IF x > 0 SELECT 1; ELSE UPDATE t SET"); + } + + static Stream opaqueStatements() { + return Stream.of(Arguments.of("shutdown defrag", "shutdown defrag"), + Arguments.of("SET IDENTITY_INSERT tb_inter_d2v_transfer on", + "SET IDENTITY_INSERT tb_inter_d2v_transfer on"), + Arguments.of("set isolation to dirty read", "set isolation to dirty read"), + Arguments.of("set isolation dirty read", "set isolation dirty read"), + Arguments.of("SET ISOLATION TO DIRTY READ WITH WARNING", + "SET ISOLATION TO DIRTY READ WITH WARNING"), + Arguments.of("SET ISOLATION TO COMMITTED READ LAST COMMITTED", + "SET ISOLATION TO COMMITTED READ LAST COMMITTED"), + Arguments.of("SET ISOLATION TO CURSOR STABILITY RETAIN UPDATE LOCKS", + "SET ISOLATION TO CURSOR STABILITY RETAIN UPDATE LOCKS"), + Arguments.of("SET ISOLATION TO REPEATABLE READ", + "SET ISOLATION TO REPEATABLE READ"), + // These are capture controls, not claims of valid dialect syntax. + Arguments.of("mystery 'a;b' /* comment */ payload", "mystery 'a;b' payload"), + Arguments.of("this is an unsupported statement", + "this is an unsupported statement")); + } + + @ParameterizedTest + @MethodSource("malformedStatements") + void malformedSupportedSyntaxIsNotOpaque(String sql) throws Exception { + for (boolean unsupported : new boolean[] {false, true}) { + for (boolean complex : new boolean[] {false, true}) { + for (String delimiter : new String[] {"", ";"}) { + String input = sql + delimiter; + assertThrowsExactly(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(input, + p -> p.withUnsupportedStatements(unsupported) + .withAllowComplexParsing(complex))); + assertThrowsExactly(JSQLParserException.class, + () -> CCJSqlParserUtil.parseStatements(input, + p -> p.withUnsupportedStatements(unsupported) + .withAllowComplexParsing(complex))); + CCJSqlParser parser = CCJSqlParserUtil.newParser(input) + .withUnsupportedStatements(unsupported) + .withAllowComplexParsing(complex); + assertThrowsExactly(ParseException.class, parser::Statement); + parser = CCJSqlParserUtil.newParser(input) + .withUnsupportedStatements(unsupported) + .withAllowComplexParsing(complex); + assertThrowsExactly(ParseException.class, parser::Statements); + } + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * FROM", "INSERT INTO t (a", "UPDATE t SET", + "IF x > 0 SELECT 1; ELSE UPDATE t SET"}) + void syntaxRecoveryRecordsErrorWithoutPublishingPartialAst(String sql) throws Exception { + for (boolean unsupported : new boolean[] {false, true}) { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql) + .withUnsupportedStatements(unsupported).withErrorRecovery(true); + assertNull(parser.Statement()); + assertEquals(1, parser.getParseErrors().size()); + for (String prefix : new String[] {"", "SELECT 0;", ";;SELECT 0;;;"}) { + parser = CCJSqlParserUtil.newParser(prefix + sql + ";;SELECT 2;") + .withUnsupportedStatements(unsupported).withErrorRecovery(true); + Statements statements = parser.Statements(); + int index = prefix.contains("SELECT") ? 1 : 0; + assertEquals(index + 2, statements.size()); + if (index > 0) { + assertEquals("SELECT 0", + assertInstanceOf(Select.class, statements.get(0)).toString()); + } + assertNull(statements.get(index)); + assertEquals("SELECT 2", + assertInstanceOf(Select.class, statements.get(index + 1)).toString()); + assertEquals(1, parser.getParseErrors().size()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * FROM", "INSERT INTO t (a", "UPDATE t SET", + "IF x > 0 SELECT 1; ELSE UPDATE t SET", "UPDATE t SET a = 'x;y',"}) + void laterMalformedStatementCannotEscapeAsPrefixAndOpaqueSuffix(String sql) { + for (boolean unsupported : new boolean[] {false, true}) { + for (boolean complex : new boolean[] {false, true}) { + assertThrowsExactly(JSQLParserException.class, + () -> CCJSqlParserUtil.parseStatements("SELECT 0;" + sql + ";SELECT 2;", + p -> p.withUnsupportedStatements(unsupported) + .withAllowComplexParsing(complex))); + } + } + } + + @ParameterizedTest + @MethodSource("opaqueStatements") + void opaqueStatementTextAndFollowingStatementsSurvive(String sql, String expected) + throws Exception { + for (boolean recovery : new boolean[] {false, true}) { + Statement result = CCJSqlParserUtil.parse("/* before */" + sql + "; -- after\n", + p -> p.withUnsupportedStatements(true).withErrorRecovery(recovery)); + assertEquals(expected, assertInstanceOf(UnsupportedStatement.class, result).toString()); + TestUtils.assertStatementCanBeDeparsedAs(result, expected, true); + for (String prefix : new String[] {"", ";;", "SELECT 0;", "SELECT 0;;;"}) { + CCJSqlParser parser = CCJSqlParserUtil.newParser(prefix + sql + ";;;SELECT 2;") + .withUnsupportedStatements(true).withErrorRecovery(recovery); + Statements statements = parser.Statements(); + int index = prefix.contains("SELECT") ? 1 : 0; + assertEquals(index + 2, statements.size()); + if (index > 0) { + assertEquals("SELECT 0", statements.get(0).toString()); + } + assertEquals(expected, + assertInstanceOf(UnsupportedStatement.class, statements.get(index)) + .toString()); + assertEquals("SELECT 2", + assertInstanceOf(Select.class, statements.get(index + 1)).toString()); + assertTrue(parser.getParseErrors().isEmpty()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * FROM", "INSERT INTO t (a", "UPDATE t SET"}) + void readerAndInputStreamUseSameRecoveryPolicy(String sql) throws Exception { + for (boolean unsupported : new boolean[] {false, true}) { + for (boolean recovery : new boolean[] {false, true}) { + for (boolean multiple : new boolean[] {false, true}) { + CCJSqlParser reader = + new CCJSqlParser(new StreamProvider(new StringReader(sql))) + .withUnsupportedStatements(unsupported) + .withErrorRecovery(recovery); + CCJSqlParser stream = CCJSqlParserUtil.newParser( + new ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8)), "UTF-8") + .withUnsupportedStatements(unsupported).withErrorRecovery(recovery); + for (CCJSqlParser parser : new CCJSqlParser[] {reader, stream}) { + if (recovery) { + assertNull(multiple ? parser.Statements().get(0) : parser.Statement()); + assertEquals(1, parser.getParseErrors().size()); + } else { + assertThrowsExactly(ParseException.class, + () -> { + if (multiple) { + parser.Statements(); + } else { + parser.Statement(); + } + }); + } + } + } + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"SET IDENTITY_INSERT = 1", "SET IDENTITY_INSERT.foo = 1", + "SET isolation = 1", "SET isolation.foo = 1"}) + void ordinarySetAssignmentsAreNotRerouted(String sql) throws Exception { + for (boolean unsupported : new boolean[] {false, true}) { + for (Dialect dialect : new Dialect[] {Dialect.SQLSERVER, Dialect.POSTGRESQL, + Dialect.EXASOL}) { + assertInstanceOf(SetStatement.class, TestUtils.assertSqlCanBeParsedAndDeparsed(sql, + true, + p -> p.withDialect(dialect).withUnsupportedStatements(unsupported))); + } + assertInstanceOf(SetStatement.class, + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + p -> p.withUnsupportedStatements(unsupported))); + } + } + + @Test + void ordinarySetToValueRemainsTypedInApplicableDialects() throws Exception { + for (boolean unsupported : new boolean[] {false, true}) { + assertInstanceOf(SetStatement.class, CCJSqlParserUtil.parse("SET isolation TO 1", + p -> p.withUnsupportedStatements(unsupported))); + assertInstanceOf(SetStatement.class, CCJSqlParserUtil.parse("SET isolation TO 1", + p -> p.withDialect(Dialect.POSTGRESQL).withUnsupportedStatements(unsupported))); + } + } + + @Test + void validTypedStatementsAndSeparatorlessAdjacencyRemainSupported() throws Exception { + for (boolean unsupported : new boolean[] {false, true}) { + for (boolean recovery : new boolean[] {false, true}) { + assertInstanceOf(Select.class, TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a FROM t", true, + p -> p.withUnsupportedStatements(unsupported).withErrorRecovery(recovery))); + assertInstanceOf(IfElseStatement.class, TestUtils.assertSqlCanBeParsedAndDeparsed( + "IF x > 0 SELECT 1 ELSE SELECT 2", true, + p -> p.withUnsupportedStatements(unsupported).withErrorRecovery(recovery))); + Statements statements = CCJSqlParserUtil.parseStatements( + "SELECT 0; SELECT 1 SELECT 2", + p -> p.withUnsupportedStatements(unsupported).withErrorRecovery(recovery)); + assertEquals(3, statements.size()); + assertEquals("SELECT 2", + assertInstanceOf(Select.class, statements.get(2)).toString()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"FROM t SELECT a, b", "FROM t |> WHERE a > 1", + "PIVOT sales ON region USING SUM(amount)", "UNPIVOT monthly_sales ON jan, feb", + "DESC table_name", "SUMMARIZE cfe.test", "VALUE (1)", "BRANCH START"}) + void alternateKnownRootsKeepTheirTypedDispatch(String sql) throws Exception { + Statement control = CCJSqlParserUtil.parse(sql); + assertTrue(!(control instanceof UnsupportedStatement)); + Statement actual = CCJSqlParserUtil.parse(sql, p -> p.withUnsupportedStatements(true)); + assertEquals(control.getClass(), actual.getClass()); + assertEquals(control.toString(), actual.toString()); + Statements script = CCJSqlParserUtil.parseStatements("SELECT 0;" + sql + ";SELECT 2;", + p -> p.withUnsupportedStatements(true)); + assertEquals(3, script.size()); + assertEquals(control.getClass(), script.get(1).getClass()); + assertEquals("SELECT 2", script.get(2).toString()); + } + + @Test + void oneStatementEntryDoesNotCaptureMultipleStatementsAsOneOpaqueValue() { + for (String sql : new String[] {"SELECT 1; SELECT 2", "shutdown defrag; SELECT 2"}) { + assertThrowsExactly(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withUnsupportedStatements(true))); + } + } + + @ParameterizedTest + @ValueSource(strings = {"SET IDENTITY_INSERT t", "SET IDENTITY_INSERT a.b.c.d ON", + "SET ISOLATION TO DIRTY", "SET IDENTITY_INSERT t ON garbage", + "SET ISOLATION TO COMMITTED READ WITH WARNING", + "SET ISOLATION TO REPEATABLE READ RETAIN UPDATE LOCKS"}) + void incompleteOpaqueVendorFormsAreRejected(String sql) { + assertThrowsExactly(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withUnsupportedStatements(true))); + assertThrowsExactly(JSQLParserException.class, + () -> CCJSqlParserUtil.parseStatements(sql, + p -> p.withUnsupportedStatements(true))); + } + + @Test + void lexicalErrorsAreNotSwallowed() { + assertThrowsExactly(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "UPDATE t SET a = 'unterminated", p -> p.withUnsupportedStatements(true))); + } + + @Test + void directSingleStatementRemainsIncremental() throws Exception { + CCJSqlParser parser = + CCJSqlParserUtil.newParser("SELECT * FROM").withUnsupportedStatements(true); + assertEquals("SELECT *", + assertInstanceOf(Select.class, parser.SingleStatement()).toString()); + assertEquals("FROM", parser.getToken(1).image); + parser = CCJSqlParserUtil.newParser("UPDATE t SET").withUnsupportedStatements(true) + .withErrorRecovery(true); + assertThrowsExactly(ParseException.class, parser::SingleStatement); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java index 4ff4c83336..095fb8c0a4 100644 --- a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java @@ -9,18 +9,30 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.create.domain.CreateDomain; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; +import java.util.function.Consumer; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class UnsupportedStatementTest { @Test @@ -115,11 +127,11 @@ public void testCaptureRestIssue1993() throws JSQLParserException { } @Test - void testAlter() throws JSQLParserException { + void testFormerlyUnsupportedAlterIndex() throws JSQLParserException { String sqlStr = "ALTER INDEX idx_t_fa RENAME TO idx_t_fb"; Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); - assertInstanceOf(UnsupportedStatement.class, statement); + assertInstanceOf(AlterRelation.class, statement); } @Test @@ -134,12 +146,12 @@ void testCreate() throws JSQLParserException { String sqlStr = "create trigger stud_marks before INSERT on Student for each row set Student.total = Student.subj1 + Student.subj2, Student.per = Student.total * 60 / 100"; Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); - assertTrue(statement instanceof UnsupportedStatement); + assertInstanceOf(CreateTrigger.class, statement); sqlStr = "create domain TNOTIFICATION_ACTION as ENUM ('ADD', 'CHANGE', 'DEL')"; statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); - assertTrue(statement instanceof UnsupportedStatement); + assertInstanceOf(CreateDomain.class, statement); } @Test @@ -192,10 +204,44 @@ void testInformixSetStatementIssue1945() throws JSQLParserException { "set isolation to dirty read;", true, parser -> parser.withUnsupportedStatements()); } + @ParameterizedTest + @ValueSource(strings = { + "CREATE LANGUAGE plperl", + "CREATE OR REPLACE TRUSTED PROCEDURAL LANGUAGE plsample HANDLER plsample_call_handler", + "CREATE OPERATOR CLASS gist__int_ops DEFAULT FOR TYPE _int4 USING gist AS OPERATOR 3 &&", + "CREATE OPERATOR FAMILY myfam USING btree", + "CREATE DEFAULT CONVERSION myconv FOR 'LATIN1' TO 'UTF8' FROM latin1_to_utf8", + "CREATE TRANSFORM FOR hstore LANGUAGE plpython3u (FROM SQL WITH FUNCTION " + + "hstore_to_plpython(internal), TO SQL WITH FUNCTION plpython_to_hstore(internal))", + "CREATE OR REPLACE AGGREGATE myavg(float8) (SFUNC = float8_accum, STYPE = float8[])"}) + void unknownCreateFallsBackToUnsupportedStatement(String sql) throws JSQLParserException { + for (Consumer dialect : Arrays.>asList(parser -> { + }, parser -> parser.withDialect(Dialect.POSTGRESQL))) { + Statement statement = CCJSqlParserUtil.parse(sql, dialect); + + assertInstanceOf(UnsupportedStatement.class, statement); + // the captured tokens render with single spaces; OR REPLACE is kept + assertEquals(sql.replaceAll("\\s+", ""), statement.toString().replaceAll("\\s+", "")); + } + } + + @Test + void unknownCreateDoesNotStopAScript() throws JSQLParserException { + Statements statements = CCJSqlParserUtil.parseStatements( + "CREATE TABLE a (x int); CREATE LANGUAGE plperl; CREATE INDEX i ON a (x);", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + + assertEquals(3, statements.size()); + assertInstanceOf(CreateTable.class, statements.get(0)); + assertInstanceOf(UnsupportedStatement.class, statements.get(1)); + assertInstanceOf(CreateIndex.class, statements.get(2)); + } + @Test void testRedshiftSetStatementIssue1708() throws JSQLParserException { Statement st = TestUtils.assertSqlCanBeParsedAndDeparsed( "SET x TO y;", true, parser -> parser.withUnsupportedStatements()); - assertInstanceOf(UnsupportedStatement.class, st); + SetStatement set = assertInstanceOf(SetStatement.class, st); + assertEquals(SetStatement.AssignmentOperator.TO, set.getAssignmentOperator()); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterActionRenderingTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterActionRenderingTest.java new file mode 100644 index 0000000000..94815435b3 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterActionRenderingTest.java @@ -0,0 +1,180 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.alter.AlterExpression.TableRenameKeyword; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterActionRenderingTest { + + static Stream drops() { + return Stream.of( + drop("DROP old_column", action -> action.setColumnName("old_column")), + drop("DROP COLUMN IF EXISTS old_column CASCADE", action -> { + action.hasColumn(true); + action.setUsingIfExists(true); + action.setColumnName("old_column"); + action.addParameters("CASCADE"); + }), + drop("DROP CONSTRAINT IF EXISTS old_constraint RESTRICT", action -> { + action.setUsingIfExists(true); + action.setConstraintName("old_constraint"); + action.addParameters("RESTRICT"); + }), + drop("DROP INDEX old_index", action -> action.setIndex( + new Index().withType("INDEX").withName("old_index"))), + drop("DROP KEY old_key", action -> action.setIndex( + new Index().withType("KEY").withName("old_key"))), + drop("DROP (a, b) CASCADE CONSTRAINTS", action -> { + action.setPkColumns(List.of("a", "b")); + action.addParameters("CASCADE", "CONSTRAINTS"); + })); + } + + private static Arguments drop(String sql, Consumer configure) { + return Arguments.of(sql, configure); + } + + @ParameterizedTest + @MethodSource("drops") + void legacyAndTypedDropActionsShareRendering(String body, Consumer configure) + throws JSQLParserException { + for (AlterExpression action : List.of(new AlterExpression(), new AlterExpressionDrop())) { + action.setOperation(AlterOperation.DROP); + configure.accept(action); + assertRoundTrip(action, body, AlterExpressionDrop.class); + } + } + + @ParameterizedTest + @EnumSource(TableRenameKeyword.class) + void legacyAndTypedTableRenamesHonorTheSelectedKeyword(TableRenameKeyword keyword) + throws JSQLParserException { + String body = "RENAME" + (keyword == TableRenameKeyword.NONE ? "" : " " + keyword) + + " db2.renamed"; + for (AlterExpression action : List.of(new AlterExpression(), new AlterExpressionRename())) { + action.setOperation(AlterOperation.RENAME_TABLE); + action.setNewTable(new Table("db2", "renamed")); + action.setTableRenameKeyword(keyword); + assertRoundTrip(action, body, AlterExpressionRename.class); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void legacyAndTypedColumnRenamesShareRendering(boolean hasColumn) throws JSQLParserException { + for (AlterExpression action : List.of(new AlterExpression(), new AlterExpressionRename())) { + action.setOperation(AlterOperation.RENAME); + action.hasColumn(hasColumn); + action.setColumnOldName("old_column"); + action.setColumnName("new_column"); + assertRoundTrip(action, "RENAME " + (hasColumn ? "COLUMN " : "") + + "old_column TO new_column", AlterExpressionRename.class); + } + } + + @ParameterizedTest + @EnumSource(value = AlterOperation.class, + names = {"RENAME_INDEX", "RENAME_KEY"}) + void legacyAndTypedIndexRenamesShareRendering(AlterOperation operation) + throws JSQLParserException { + for (AlterExpression action : List.of(new AlterExpression(), new AlterExpressionRename())) { + action.setOperation(operation); + action.setOldIndex(new Index().withName("old_name")); + action.setIndex(new Index().withName("new_name")); + assertRoundTrip(action, operation.toString().replace('_', ' ') + + " old_name TO new_name", AlterExpressionRename.class); + } + } + + @Test + void legacyAndTypedConstraintRenamesShareRendering() throws JSQLParserException { + for (AlterExpression action : List.of(new AlterExpression(), new AlterExpressionRename())) { + action.setOperation(AlterOperation.RENAME_CONSTRAINT); + action.setConstraintName("old_name"); + action.setNewConstraintName("new_name"); + assertRoundTrip(action, "RENAME CONSTRAINT old_name TO new_name", + AlterExpressionRename.class); + } + } + + @Test + void commonTailIsAppendedOnceForTypedAndLegacyActions() { + for (AlterExpression action : List.of(new AlterExpression(), new AlterExpressionDrop())) { + Index index = new Index().withType("INDEX").withName("old_index"); + index.setCommentText("'index comment'"); + action.setOperation(AlterOperation.DROP); + action.setIndex(index); + action.addParameters("CASCADE"); + assertEquals("DROP INDEX old_index CASCADE COMMENT 'index comment'", action.toString()); + StringBuilder output = new StringBuilder(); + statement(action).accept(new StatementDeParser(output), null); + assertEquals("ALTER TABLE t DROP INDEX old_index CASCADE COMMENT 'index comment'", + output.toString()); + } + } + + @Test + void mixedActionsStillUseTheCustomExpressionDeparser() throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse("ALTER TABLE t DROP COLUMN obsolete, " + + "RENAME COLUMN old_column TO new_column, ADD COLUMN amount INT DEFAULT (1 + 2)"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + alter.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + String expected = "ALTER TABLE t DROP COLUMN obsolete, " + + "RENAME COLUMN old_column TO new_column, ADD COLUMN amount INT DEFAULT (101 + 102)"; + assertEquals(expected, output.toString()); + assertEquals(expected, CCJSqlParserUtil.parse(output.toString()).toString()); + } + + private static Alter statement(AlterExpression action) { + Alter alter = new Alter(); + alter.setTable(new Table("t")); + alter.addAlterExpression(action); + return alter; + } + + private static void assertRoundTrip(AlterExpression action, String expectedBody, + Class parsedType) throws JSQLParserException { + String expected = "ALTER TABLE t " + expectedBody; + Alter alter = statement(action); + assertEquals(expected, alter.toString()); + StringBuilder output = new StringBuilder(); + alter.accept(new StatementDeParser(output), null); + assertEquals(expected, output.toString()); + Alter reparsed = (Alter) CCJSqlParserUtil.parse(output.toString()); + assertInstanceOf(parsedType, reparsed.getAlterExpressions().get(0)); + assertEquals(expected, reparsed.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java new file mode 100644 index 0000000000..ce41db3d31 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java @@ -0,0 +1,128 @@ +/*- + * #%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.statement.alter; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * ALTER TABLE ADD accepts the table constraint forms of CREATE TABLE in every dialect, not only + * with the MySQL and PostgreSQL dialects. + */ +class AlterAddTableConstraintTest { + private static final List DIALECTS = + Arrays.asList(null, Dialect.ANSI_SQL, Dialect.ORACLE, Dialect.SQLSERVER, Dialect.H2); + + @ParameterizedTest + @ValueSource(strings = { + "CHECK (id > 0)", + "CHECK (id > 0) NOT VALID", + "CONSTRAINT ck CHECK (id > 0) NOT VALID", + "FOREIGN KEY (id) REFERENCES parent (id) DEFERRABLE INITIALLY DEFERRED", + "CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent (id) NOT VALID", + "CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED", + "CONSTRAINT uq UNIQUE (id) DEFERRABLE INITIALLY IMMEDIATE", + "CONSTRAINT ex EXCLUDE USING gist (id WITH =)"}) + void createAndAlterExposeTheSameConstraintInEveryDialect(String definition) + throws JSQLParserException { + for (Dialect dialect : DIALECTS) { + Consumer configuration = parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }; + CreateTable create = (CreateTable) CCJSqlParserUtil + .parse("CREATE TABLE t (id INT, " + definition + ")", configuration); + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t ADD " + definition + ", ADD COLUMN other INT", true, + configuration); + NamedConstraint expected = create.getTableConstraints().get(0); + NamedConstraint actual = alter.getAlterExpressions().get(0).getConstraint(); + assertEquals(expected.getClass(), actual.getClass(), String.valueOf(dialect)); + assertEquals(expected.toString(), actual.toString(), String.valueOf(dialect)); + assertEquals(2, alter.getAlterExpressions().size()); + } + } + + @Test + void attributesAndReferencesAreStructuredWithoutADialect() throws JSQLParserException { + String sql = "ALTER TABLE t ADD CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent (id) " + + "DEFERRABLE INITIALLY DEFERRED"; + Alter alter = (Alter) CCJSqlParserUtil.parse(sql); + ForeignKeyConstraint foreignKey = assertInstanceOf(ForeignKeyConstraint.class, + alter.getAlterExpressions().get(0).getConstraint()); + ConstraintAttributes attributes = foreignKey.getConstraintAttributes(); + assertEquals(Boolean.TRUE, attributes.getDeferrable()); + assertEquals(ConstraintAttributes.Initially.DEFERRED, attributes.getInitially()); + assertThat(TablesNamesFinder.findTables(sql)).containsExactlyInAnyOrder("t", "parent"); + + CheckConstraint check = assertInstanceOf(CheckConstraint.class, + ((Alter) CCJSqlParserUtil.parse("ALTER TABLE t ADD CHECK (id > 0) NOT VALID")) + .getAlterExpressions().get(0).getConstraint()); + assertNull(check.getName()); + assertTrue(check.getConstraintAttributes().isNotValid()); + } + + @Test + void formsThatAlreadyParsedKeepTheGenericRepresentation() throws JSQLParserException { + AlterExpression deferrable = ((Alter) CCJSqlParserUtil + .parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE")) + .getAlterExpressions().get(0); + assertThat(deferrable.getConstraints()).hasSize(1); + assertInstanceOf(DeferrableConstraint.class, deferrable.getConstraints().get(0)); + + AlterExpression unique = ((Alter) CCJSqlParserUtil + .parse("ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uk (id)")) + .getAlterExpressions().get(0); + assertEquals("UNIQUE KEY", unique.getConstraintType()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ADD CONSTRAINT nn NOT NULL id", + "ALTER TABLE t ADD CONSTRAINT uk UNIQUE USING INDEX t_idx"}) + void postgreSqlOnlyFormsStillRequireThePostgreSqlDialect(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertDoesNotThrow( + () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + } + + @Test + void enforcedPrimaryKeyIsRejected() { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil + .parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) ENFORCED")); + assertThat(exception).hasStackTraceContaining( + "ENFORCED is supported only for CHECK and foreign key constraints"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterColumnDefaultExpressionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterColumnDefaultExpressionTest.java new file mode 100644 index 0000000000..07cbcc8972 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterColumnDefaultExpressionTest.java @@ -0,0 +1,101 @@ +/*- + * #%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.statement.alter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetDefault; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterColumnDefaultExpressionTest { + @ParameterizedTest + @ValueSource(strings = {"(1 + 2)", "NULL", "CURRENT_TIMESTAMP", + "nextval('app.counter'::regclass)", "'value'::text", "-1"}) + void parsedDefaultRetainsExpressionAndLegacyText(String value) throws JSQLParserException { + Alter alter = parse(value); + ColumnSetDefault column = + alter.getAlterExpressions().get(0).getColumnSetDefaultList().get(0); + assertNotNull(column.getDefaultExpression()); + assertEquals(column.getDefaultExpression().toString(), column.getDefaultValue()); + StringBuilder buffer = new StringBuilder(); + alter.accept(new StatementDeParser(buffer), null); + assertEquals(alter.toString(), buffer.toString()); + assertEquals(alter.toString(), CCJSqlParserUtil.parse(buffer.toString()).toString()); + } + + @Test + void visitorReachesDefaultsInEachAlterActionWithContext() throws JSQLParserException { + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }; + CCJSqlParserUtil.parse("ALTER TABLE t ALTER COLUMN a SET DEFAULT (1 + 2), " + + "ALTER COLUMN b SET DEFAULT 3") + .accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "context"); + assertThat(values).containsExactly(1L, 2L, 3L); + } + + @Test + void customDeparserAndAstEditsUseTheStructuredDefault() throws JSQLParserException { + Alter alter = parse("(1 + 2)"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + alter.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals("ALTER TABLE t ALTER COLUMN a SET DEFAULT (101 + 102)", output.toString()); + ColumnSetDefault column = + alter.getAlterExpressions().get(0).getColumnSetDefaultList().get(0); + column.setDefaultExpression(new LongValue(42)); + assertEquals("42", column.getDefaultValue()); + assertEquals("ALTER TABLE t ALTER COLUMN a SET DEFAULT 42", alter.toString()); + } + + @Test + void legacyStringConstructorRemainsOpaqueAndAcceptsNull() { + ColumnSetDefault column = new ColumnSetDefault("a", "vendor_default()"); + assertNull(column.getDefaultExpression()); + assertEquals("a SET DEFAULT vendor_default()", column.toString()); + assertEquals("a SET DEFAULT null", new ColumnSetDefault("a", null).toString()); + column.setDefaultExpression(new LongValue(1)); + assertEquals("1", column.getDefaultValue()); + column.setDefaultExpression(null); + assertNull(column.getDefaultValue()); + } + + private static Alter parse(String value) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse("ALTER TABLE t ALTER COLUMN a SET DEFAULT " + value); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterCommentColumnTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterCommentColumnTest.java new file mode 100644 index 0000000000..fc74b4e30a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterCommentColumnTest.java @@ -0,0 +1,91 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterCommentColumnTest { + @ParameterizedTest + @ValueSource(strings = { + "ADD COLUMN comment TEXT", "ADD comment TEXT", "MODIFY COLUMN comment TEXT", + "MODIFY comment TEXT", "MODIFY COLUMN `comment` TEXT", + "MODIFY COLUMN comment TEXT COMMENT 'note' AFTER id", + "CHANGE COLUMN comment comment2 TEXT", "CHANGE COLUMN old_name comment TEXT", + "DROP COLUMN comment", "DROP comment", "RENAME COLUMN comment TO comment2", + "RENAME COLUMN old_name TO comment" + }) + void roundTripsUnquotedCommentColumn(String action) throws JSQLParserException { + for (Dialect dialect : new Dialect[] {null, Dialect.MYSQL}) { + Alter alter = parse("ALTER TABLE t " + action, dialect); + assertEquals(1, alter.getAlterExpressions().size()); + assertRoundTrip(alter, dialect); + } + } + + @Test + void exposesEditableColumnDefinitionInsteadOfColumnComment() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t MODIFY COLUMN comment TEXT", Dialect.MYSQL); + AlterExpression action = alter.getAlterExpressions().get(0); + assertEquals(AlterOperation.MODIFY, action.getOperation()); + assertNull(action.getCommentText()); + AlterExpression.ColumnDataType column = action.getColDataTypeList().get(0); + assertEquals("comment", column.getColumnName()); + assertEquals("TEXT", column.getColDataType().getDataType()); + column.setColumnName("notes"); + column.getColDataType().setDataType("LONGTEXT"); + assertEquals("ALTER TABLE t MODIFY COLUMN notes LONGTEXT", alter.toString()); + assertRoundTrip(alter, Dialect.MYSQL); + } + + @Test + void exposesBothNamesInRenameAndChange() throws JSQLParserException { + Alter rename = parse("ALTER TABLE t RENAME COLUMN comment TO notes", Dialect.MYSQL); + AlterExpression action = rename.getAlterExpressions().get(0); + assertEquals("comment", action.getColumnOldName()); + assertEquals("notes", action.getColumnName()); + action.setColumnName("remarks"); + assertEquals("ALTER TABLE t RENAME COLUMN comment TO remarks", rename.toString()); + assertRoundTrip(rename, Dialect.MYSQL); + Alter change = parse("ALTER TABLE t CHANGE COLUMN comment notes TEXT", Dialect.MYSQL); + assertEquals("comment", change.getAlterExpressions().get(0).getColumnOldName()); + assertEquals("notes", + change.getAlterExpressions().get(0).getColDataTypeList().get(0).getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER c COMMENT 'description'", "COMMENT = 'table description'", + "DROP COLUMN type", "DROP INDEX idx", "DROP CONSTRAINT ck", "RENAME TO other", + "RENAME INDEX idx TO other_idx", "RENAME CONSTRAINT ck TO other_ck"}) + void preservesOtherAlterBranches(String action) throws JSQLParserException { + assertRoundTrip(parse("ALTER TABLE t " + action, null), null); + } + + private static Alter parse(String sql, Dialect dialect) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, p -> { + if (dialect != null) { + p.withDialect(dialect); + } + }); + } + + private static void assertRoundTrip(Alter alter, Dialect dialect) throws JSQLParserException { + StringBuilder buffer = new StringBuilder(); + alter.accept(new StatementDeParser(buffer), null); + assertEquals(alter.toString(), buffer.toString()); + assertEquals(alter.toString(), parse(buffer.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterConstraintSeparationTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterConstraintSeparationTest.java new file mode 100644 index 0000000000..f568b6eba5 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterConstraintSeparationTest.java @@ -0,0 +1,201 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.List; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterConstraintSeparationTest { + @Test + void uniqueConstraintAndOrdinaryIndexHaveSeparateEditableDefinitions() throws Exception { + Alter statement = parse("ALTER TABLE t ADD UNIQUE INDEX uq (id), ADD INDEX ix (other)", + Dialect.MYSQL); + AlterExpression uniqueAction = statement.getAlterExpressions().get(0); + KeyConstraint unique = assertInstanceOf(KeyConstraint.class, uniqueAction.getConstraint()); + assertNull(uniqueAction.getIndex()); + assertEquals(ConstraintKind.UNIQUE, uniqueAction.getConstraintKind()); + assertEquals("uq", unique.getIndexName()); + assertNull(unique.getName()); + + AlterExpression indexAction = statement.getAlterExpressions().get(1); + assertNull(indexAction.getConstraint()); + assertEquals(ConstraintKind.OTHER, indexAction.getConstraintKind()); + assertEquals(Index.Kind.INDEX, indexAction.getIndex().getKind()); + unique.setName("unique_symbol"); + unique.setIndexName("new_uq"); + assertEquals("new_uq", uniqueAction.getUkName()); + uniqueAction.setUkName("legacy_uq"); + assertEquals("legacy_uq", unique.getIndexName()); + uniqueAction.setUkColumns(List.of("new_id")); + indexAction.getIndex().setName("new_ix"); + assertRoundTrip(statement, + "ALTER TABLE t ADD CONSTRAINT unique_symbol UNIQUE INDEX legacy_uq (new_id), " + + "ADD INDEX new_ix (other)", + Dialect.MYSQL); + } + + @Test + void genericConstraintKeywordPathExposesACompleteUniqueDefinition() throws Exception { + Alter statement = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uq (id)"); + AlterExpression action = statement.getAlterExpressions().get(0); + KeyConstraint key = assertInstanceOf(KeyConstraint.class, action.getConstraint()); + assertNull(action.getIndex()); + assertEquals(ConstraintKind.UNIQUE, key.getKind()); + assertEquals("UNIQUE KEY", key.getType()); + assertEquals("uq", key.getIndexName()); + assertEquals("CONSTRAINT UNIQUE KEY uq (id)", key.toString()); + action.setUkColumns(List.of("new_id")); + key.setIndexName("renamed_uq"); + key.setType("UNIQUE INDEX"); + assertEquals("renamed_uq", action.getConstraintSymbol()); + assertEquals("UNIQUE INDEX", action.getConstraintType()); + assertEquals("CONSTRAINT UNIQUE INDEX renamed_uq (new_id)", key.toString()); + action.setConstraintSymbol("legacy_uq"); + assertEquals("legacy_uq", key.getIndexName()); + action.setConstraintType("UNIQUE KEY"); + assertEquals("UNIQUE KEY", key.getType()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(statement.toString(), deparsed.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(deparsed.toString()).toString()); + } + + @Test + void replacingDefinitionChangesTheRenderedActionAndItsClassification() throws Exception { + Alter statement = parse("ALTER TABLE t ADD INDEX ix (id)", Dialect.MYSQL); + AlterExpression action = statement.getAlterExpressions().get(0); + action.setConstraint(new KeyConstraint().withType("PRIMARY KEY") + .withColumnsNames(List.of("id"))); + assertNull(action.getIndex()); + assertEquals(ConstraintKind.PRIMARY_KEY, action.getConstraintKind()); + action.setIndex(null); + assertRoundTrip(statement, "ALTER TABLE t ADD PRIMARY KEY (id)", Dialect.MYSQL); + + action.setIndex( + new Index().withIndexKeyword("INDEX").withKind(Index.Kind.INDEX).withName("ix_new") + .withColumnsNames(List.of("id"))); + assertNull(action.getConstraint()); + action.setConstraint(null); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + assertRoundTrip(statement, "ALTER TABLE t ADD INDEX ix_new (id)", Dialect.MYSQL); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void replacingLegacyUniqueDefinitionClearsItsOldHeader(boolean replaceWithIndex) + throws Exception { + Alter statement = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uq (id)"); + AlterExpression action = statement.getAlterExpressions().get(0); + String original = statement.toString(); + action.setConstraint(action.getConstraint()); + assertEquals(original, statement.toString()); + if (replaceWithIndex) { + action.setIndex(new Index().withIndexKeyword("INDEX").withKind(Index.Kind.INDEX) + .withName("ix").withColumnsNames(List.of("id"))); + assertNull(action.getConstraint()); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + assertRoundTrip(statement, "ALTER TABLE t ADD INDEX ix (id)", Dialect.MYSQL); + } else { + action.setConstraint(new CheckConstraint() + .withExpression(CCJSqlParserUtil.parseCondExpression("id > 0"))); + assertNull(action.getIndex()); + assertEquals(ConstraintKind.CHECK, action.getConstraintKind()); + assertRoundTrip(statement, "ALTER TABLE t ADD CHECK (id > 0)", Dialect.MYSQL); + } + assertNull(action.getConstraintType()); + assertNull(action.getConstraintSymbol()); + } + + @Test + void clearingLegacyUniqueDefinitionClearsItsHeaderAliases() throws Exception { + Alter statement = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uq (id)"); + AlterExpression action = statement.getAlterExpressions().get(0); + action.setConstraint(null); + assertNull(action.getConstraint()); + assertNull(action.getConstraintType()); + assertNull(action.getConstraintSymbol()); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + } + + @Test + void namedConstraintDropDoesNotInferDefinitionFromItsNameOrInactiveFields() throws Exception { + Alter statement = parse("ALTER TABLE t DROP CONSTRAINT pk_t", Dialect.POSTGRESQL); + AlterExpression action = statement.getAlterExpressions().get(0); + assertNull(action.getConstraint()); + assertNull(action.getIndex()); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + action.setConstraint(new CheckConstraint()); + action.setConstraintName("new_constraint"); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + assertRoundTrip(statement, "ALTER TABLE t DROP CONSTRAINT new_constraint", + Dialect.POSTGRESQL); + } + + @Test + void constraintRenameUsesNamesWithoutCreatingIndexDefinitions() throws Exception { + Alter statement = parse("ALTER TABLE t RENAME CONSTRAINT old_name TO new_name", + Dialect.POSTGRESQL); + AlterExpression action = statement.getAlterExpressions().get(0); + assertNull(action.getIndex()); + assertNull(action.getOldIndex()); + assertNull(action.getConstraint()); + assertEquals("old_name", action.getConstraintName()); + assertEquals("new_name", action.getNewConstraintName()); + action.setConstraintName("source_name"); + action.setNewConstraintName("target_name"); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + assertRoundTrip(statement, "ALTER TABLE t RENAME CONSTRAINT source_name TO target_name", + Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = {"INDEX", "KEY"}) + void indexRenameRetainsIndexTargetsAndKeyword(String keyword) throws Exception { + Alter statement = parse("ALTER TABLE t RENAME " + keyword + " old_name TO new_name", + Dialect.MYSQL); + AlterExpression action = statement.getAlterExpressions().get(0); + assertNull(action.getConstraint()); + assertNull(action.getConstraintName()); + assertNull(action.getNewConstraintName()); + action.getOldIndex().setName("source_name"); + action.getIndex().setName("target_name"); + assertRoundTrip(statement, + "ALTER TABLE t RENAME " + keyword + " source_name TO target_name", Dialect.MYSQL); + } + + private static Alter parse(String sql, Dialect dialect) throws Exception { + return (Alter) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static void assertRoundTrip(Alter statement, String sql, Dialect dialect) + throws Exception { + assertEquals(sql, statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(sql, deparsed.toString()); + assertEquals(sql, parse(deparsed.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterDropBehaviorTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterDropBehaviorTest.java new file mode 100644 index 0000000000..3a0263388c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterDropBehaviorTest.java @@ -0,0 +1,143 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterDropBehaviorTest { + private static final Dialect[] DIALECTS = {Dialect.POSTGRESQL, null}; + + @ParameterizedTest + @CsvSource(value = {"DROP COLUMN b RESTRICT|RESTRICT", "DROP b RESTRICT|RESTRICT", + "DROP COLUMN IF EXISTS b RESTRICT|RESTRICT", "DROP IF EXISTS b CASCADE|CASCADE", + "DROP COLUMN b CASCADE|CASCADE", "DROP COLUMN b|"}, delimiter = '|') + void columnDropsExposeTheirBehavior(String action, CascadeBehavior behavior) + throws JSQLParserException { + String sql = "ALTER TABLE users " + action; + for (Dialect dialect : DIALECTS) { + Alter alter = parse(sql, dialect); + AlterExpressionDrop drop = + assertInstanceOf(AlterExpressionDrop.class, alter.getAlterExpressions().get(0)); + assertEquals("b", drop.getColumnName()); + assertEquals(action.contains("IF EXISTS"), drop.isUsingIfExists()); + assertEquals(behavior, drop.getBehavior()); + assertEquals(behavior == null ? null : List.of(behavior.name()), + drop.getParameters()); + assertRoundTrip(alter, sql, dialect); + } + } + + @Test + void restrictAppliesToItsOwnAction() throws JSQLParserException { + String sql = "ALTER TABLE users DROP a, DROP COLUMN b RESTRICT"; + for (Dialect dialect : DIALECTS) { + Alter alter = parse(sql, dialect); + assertNull(((AlterExpressionDrop) alter.getAlterExpressions().get(0)).getBehavior()); + assertEquals(CascadeBehavior.RESTRICT, + ((AlterExpressionDrop) alter.getAlterExpressions().get(1)).getBehavior()); + assertRoundTrip(alter, sql, dialect); + } + } + + @ParameterizedTest + @CsvSource(value = {"DROP CONSTRAINT IF EXISTS c RESTRICT|RESTRICT", + "DROP CONSTRAINT c CASCADE|CASCADE", "DROP PRIMARY KEY CASCADE|CASCADE", + "DROP UNIQUE (a, b) RESTRICT|RESTRICT", "DROP FOREIGN KEY (a) CASCADE|CASCADE"}, + delimiter = '|') + void constraintDropsShareTheBehavior(String action, CascadeBehavior behavior) + throws JSQLParserException { + String sql = "ALTER TABLE t " + action; + AlterExpressionDrop drop = assertInstanceOf(AlterExpressionDrop.class, + parse(sql, null).getAlterExpressions().get(0)); + assertEquals(behavior, drop.getBehavior()); + assertEquals(List.of(behavior.name()), drop.getParameters()); + assertRoundTrip(parse(sql, null), sql, null); + } + + @ParameterizedTest + @CsvSource(value = {"DROP COLUMN c CASCADE CONSTRAINTS||CASCADE,CONSTRAINTS", + "DROP COLUMN c INVALIDATE CASCADE CONSTRAINTS||INVALIDATE,CASCADE,CONSTRAINTS", + "DROP COLUMN c INVALIDATE CASCADE|CASCADE|INVALIDATE,CASCADE", + "DROP (a, b) CASCADE CONSTRAINTS||CASCADE,CONSTRAINTS"}, delimiter = '|') + void oracleClausesRemainRawParameters(String action, CascadeBehavior behavior, + String parameters) throws JSQLParserException { + String sql = "ALTER TABLE t " + action; + for (Dialect dialect : new Dialect[] {Dialect.ORACLE, null}) { + Alter alter = parse(sql, dialect); + AlterExpressionDrop drop = (AlterExpressionDrop) alter.getAlterExpressions().get(0); + assertEquals(behavior, drop.getBehavior()); + assertEquals(List.of(parameters.split(",")), drop.getParameters()); + assertRoundTrip(alter, sql, dialect); + } + } + + @Test + void editingTheBehaviorUpdatesBothRenderersAndLegacyParameters() + throws JSQLParserException { + Alter alter = parse("ALTER TABLE users DROP COLUMN b RESTRICT", Dialect.POSTGRESQL); + AlterExpressionDrop drop = (AlterExpressionDrop) alter.getAlterExpressions().get(0); + assertThrows(UnsupportedOperationException.class, () -> drop.getParameters().add("X")); + + drop.setBehavior(CascadeBehavior.CASCADE); + assertEquals(List.of("CASCADE"), drop.getParameters()); + assertRoundTrip(alter, "ALTER TABLE users DROP COLUMN b CASCADE", Dialect.POSTGRESQL); + + drop.setBehavior(null); + assertNull(drop.getParameters()); + assertRoundTrip(alter, "ALTER TABLE users DROP COLUMN b", Dialect.POSTGRESQL); + + AlterExpressionDrop built = + new AlterExpressionDrop().withBehavior(CascadeBehavior.RESTRICT); + built.setOperation(AlterOperation.DROP); + built.hasColumn(true); + built.setColumnName("b"); + Alter statement = new Alter(); + statement.setTable(new Table("users")); + statement.addAlterExpression(built); + assertRoundTrip(statement, "ALTER TABLE users DROP COLUMN b RESTRICT", Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP COLUMN b RESTRICT CASCADE", "DROP COLUMN b CASCADE RESTRICT", + "DROP COLUMN b RESTRICT RESTRICT"}) + void rejectsRepeatedBehaviors(String action) { + for (Dialect dialect : DIALECTS) { + assertThrows(JSQLParserException.class, + () -> parse("ALTER TABLE users " + action, dialect)); + } + } + + private static Alter parse(String sql, Dialect dialect) throws JSQLParserException { + return (Alter) (dialect == null + ? CCJSqlParserUtil.parse(sql) + : CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect))); + } + + private static void assertRoundTrip(Alter alter, String expected, Dialect dialect) + throws JSQLParserException { + assertEquals(expected, alter.toString()); + StringBuilder deparsed = new StringBuilder(); + alter.accept(new StatementDeParser(deparsed), null); + assertEquals(expected, deparsed.toString()); + assertEquals(expected, parse(deparsed.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterForeignKeyStateTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterForeignKeyStateTest.java new file mode 100644 index 0000000000..75fe0a0105 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterForeignKeyStateTest.java @@ -0,0 +1,306 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Database; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.ReferentialAction; +import net.sf.jsqlparser.statement.ReferentialAction.Action; +import net.sf.jsqlparser.statement.ReferentialAction.Type; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.ForeignKeyReference; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterForeignKeyStateTest { + private static final String FK_SQL = "ALTER TABLE child ADD CONSTRAINT fk FOREIGN KEY " + + "(parent_id) REFERENCES app.parent(id) ON DELETE CASCADE"; + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "POSTGRESQL"}) + void legacyEditsUpdateParsedForeignKeysAndBothRenderers(Dialect dialect) throws Exception { + Alter statement = parse(FK_SQL, dialect); + AlterExpression action = first(statement); + ForeignKeyConstraint foreignKey = foreignKey(action); + assertEquals(List.of("parent_id"), action.getFkColumns()); + assertEquals("app", action.getFkSourceSchema()); + assertEquals("parent", action.getFkSourceTable()); + assertSame(foreignKey.getReferencedColumnNames(), action.getFkSourceColumns()); + assertSame(foreignKey.getReferentialAction(Type.DELETE), + action.getReferentialAction(Type.DELETE)); + + action.setFkColumns(List.of("owner_id")); + action.setFkSourceSchema("archive"); + action.setFkSourceTable("owners"); + action.setFkSourceColumns(new ArrayList<>(List.of("owner_key"))); + action.setReferentialAction(Type.DELETE, Action.RESTRICT); + + assertSame(foreignKey, action.getConstraint()); + assertEquals("fk", foreignKey.getName()); + assertRoundTrip("ALTER TABLE child ADD CONSTRAINT fk FOREIGN KEY (owner_id) " + + "REFERENCES archive.owners(owner_key) ON DELETE RESTRICT", statement, dialect); + } + + @Test + void actionColumnListsAndAttributesSurviveLegacyAndStructuredMutations() throws Exception { + Alter statement = parse("ALTER TABLE posts ADD CONSTRAINT fk FOREIGN KEY " + + "(tenant_id, author_id) REFERENCES users(tenant_id, id) MATCH SIMPLE " + + "ON DELETE SET NULL (author_id) DEFERRABLE INITIALLY DEFERRED NOT VALID", + Dialect.POSTGRESQL); + AlterExpression action = first(statement); + ForeignKeyConstraint foreignKey = foreignKey(action); + ReferentialAction delete = foreignKey.getReferentialAction(Type.DELETE); + assertSame(delete, action.getReferentialAction(Type.DELETE)); + action.setOnDeleteSetNull(true); + assertSame(delete, action.getReferentialAction(Type.DELETE)); + assertEquals(List.of("author_id"), delete.getColumnNames()); + + action.getReferentialAction(Type.DELETE).setColumnNames(List.of("tenant_id")); + action.setReferentialAction(Type.DELETE, Action.SET_DEFAULT); + assertSame(delete, foreignKey.getReferentialAction(Type.DELETE)); + assertEquals(List.of("tenant_id"), delete.getColumnNames()); + assertRoundTrip("ALTER TABLE posts ADD CONSTRAINT fk FOREIGN KEY " + + "(tenant_id, author_id) REFERENCES users(tenant_id, id) MATCH SIMPLE " + + "ON DELETE SET DEFAULT (tenant_id) DEFERRABLE INITIALLY DEFERRED NOT VALID", + statement, Dialect.POSTGRESQL); + + delete.setAction(Action.CASCADE); + action.removeReferentialAction(Type.DELETE); + assertNull(foreignKey.getReferentialAction(Type.DELETE)); + assertFalse(action.isOnDeleteSetNull()); + assertRoundTrip("ALTER TABLE posts ADD CONSTRAINT fk FOREIGN KEY " + + "(tenant_id, author_id) REFERENCES users(tenant_id, id) MATCH SIMPLE " + + "DEFERRABLE INITIALLY DEFERRED NOT VALID", statement, Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void builderSetterOrderAndFluentSchemaPreserveOneReference(boolean schemaFirst) + throws Exception { + AlterExpression action = new AlterExpression(); + action.setReferentialAction(Type.DELETE, Action.CASCADE); + if (schemaFirst) { + action.withFkSourceSchema("archive").withFkSourceTable("parents"); + } else { + action.withFkSourceTable("parents").withFkSourceSchema("archive"); + } + action.withFkColumns(List.of("parent_id")).withFkSourceColumns(List.of("id")); + assertNull(action.getOperation()); + action.setOperation(AlterOperation.ADD); + assertEquals("parents", action.getFkSourceTable()); + assertEquals("archive", action.getFkSourceSchema()); + assertEquals(ConstraintKind.FOREIGN_KEY, action.getConstraintKind()); + assertRoundTrip("ALTER TABLE child ADD FOREIGN KEY (parent_id) " + + "REFERENCES archive.parents(id) ON DELETE CASCADE", statement(action), + Dialect.POSTGRESQL); + } + + @Test + void keySnapshotsAndFluentAdditionsUseTheCurrentStructuredColumns() throws Exception { + Alter statement = parse(FK_SQL, Dialect.POSTGRESQL); + AlterExpression action = first(statement); + ForeignKeyConstraint foreignKey = foreignKey(action); + foreignKey.setColumnsNames(List.of("account_id")); + List snapshot = action.getFkColumns(); + assertEquals(List.of("account_id"), snapshot); + snapshot.set(0, "detached"); + assertEquals(List.of("account_id"), action.getFkColumns()); + action.addFkColumns("tenant_id").addFkColumns(List.of("region_id")); + action.addFkSourceColumns("tenant_id").addFkSourceColumns(List.of("region_id")); + action.getFkSourceColumns().set(0, "account_key"); + assertSame(foreignKey.getReferencedColumnNames(), action.getFkSourceColumns()); + assertEquals(List.of("account_id", "tenant_id", "region_id"), + foreignKey.getColumnsNames()); + assertRoundTrip("ALTER TABLE child ADD CONSTRAINT fk FOREIGN KEY " + + "(account_id, tenant_id, region_id) REFERENCES app.parent" + + "(account_key, tenant_id, region_id) ON DELETE CASCADE", statement, + Dialect.POSTGRESQL); + } + + @Test + void referenceReplacementAndNameEditsPreserveOtherQualifiedNameParts() throws Exception { + AlterExpression action = first(parse(FK_SQL, Dialect.POSTGRESQL)); + Table table = new Table("new_schema", "new_parent"); + table.setDatabase(new Database("catalog")); + ForeignKeyReference replacement = new ForeignKeyReference().withTable(table) + .withReferencedColumnNames(new ArrayList<>(List.of("new_id"))) + .withReferentialAction(Type.UPDATE, Action.NO_ACTION); + foreignKey(action).setReference(replacement); + assertEquals("new_schema", action.getFkSourceSchema()); + assertEquals("new_parent", action.getFkSourceTable()); + assertSame(replacement.getReferencedColumnNames(), action.getFkSourceColumns()); + assertNull(action.getReferentialAction(Type.DELETE)); + assertSame(replacement.getReferentialAction(Type.UPDATE), + action.getReferentialAction(Type.UPDATE)); + + action.setFkSourceSchema("\"changed.schema\""); + action.setFkSourceTable("\"changed.parent\""); + assertSame(table, replacement.getTable()); + assertEquals("catalog", table.getDatabase().getDatabaseName()); + assertEquals("\"changed.schema\"", table.getSchemaName()); + assertEquals("\"changed.parent\"", table.getName()); + } + + @Test + void replacingOrClearingTheConstraintDoesNotRestoreDetachedForeignKeyState() + throws Exception { + AlterExpression action = first(parse(FK_SQL, Dialect.POSTGRESQL)); + ForeignKeyConstraint old = foreignKey(action); + ForeignKeyConstraint replacement = new ForeignKeyConstraint() + .withColumnsNames(List.of("replacement_id")) + .withTable(new Table("replacement_parent")) + .withReferencedColumnNames(List.of("id")); + action.setConstraint(replacement); + assertEquals(List.of("replacement_id"), action.getFkColumns()); + assertEquals("replacement_parent", action.getFkSourceTable()); + assertNull(action.getFkSourceSchema()); + assertNull(action.getReferentialAction(Type.DELETE)); + assertEquals(Action.CASCADE, old.getReferentialAction(Type.DELETE).getAction()); + action.setConstraint(null); + assertNoForeignKeyValues(action); + action.setFkSourceTable("fresh_parent"); + assertNull(action.getFkColumns()); + assertNull(action.getFkSourceColumns()); + assertNull(action.getReferentialAction(Type.DELETE)); + assertEquals("fresh_parent", foreignKey(action).getTable().getName()); + } + + @Test + void absentReadsClearsAndRemovalsDoNotCreateAForeignKey() { + AlterExpression action = new AlterExpression(); + assertNoForeignKeyValues(action); + clearForeignKeyValues(action); + assertNull(action.getConstraint()); + assertNull(action.getIndex()); + assertNull(action.getOperation()); + } + + @Test + void storedForeignKeyDefinitionDoesNotChangeADropTarget() throws Exception { + Alter statement = parse("ALTER TABLE child DROP FOREIGN KEY fk", Dialect.MYSQL); + AlterExpression action = first(statement); + ForeignKeyConstraint stale = new ForeignKeyConstraint().withName("stale") + .withColumnsNames(List.of("unused")) + .withTable(new Table("unused_parent")) + .withReferencedColumnNames(List.of("unused_id")); + action.setConstraint(stale); + assertSame(stale, action.getConstraint()); + assertEquals(ConstraintKind.FOREIGN_KEY, action.getConstraintKind()); + assertRoundTrip("ALTER TABLE child DROP FOREIGN KEY fk", statement, Dialect.MYSQL); + } + + static Stream nonForeignKeyDefinitions() { + return Stream.of( + new AlterExpression().withOperation(AlterOperation.ADD).withIndex( + new Index().withType("INDEX").withName("ix") + .withColumnsNames(List.of("id"))), + new AlterExpression().withOperation(AlterOperation.ADD).withConstraint( + new KeyConstraint().withType("UNIQUE").withColumnsNames(List.of("id"))), + new AlterExpression().withOperation(AlterOperation.ADD) + .withPkColumns(List.of("id")), + new AlterExpression().withOperation(AlterOperation.ADD) + .withUkColumns(List.of("id")), + new AlterExpressionPrimaryKey().withPkColumns(List.of("id"))); + } + + @ParameterizedTest + @MethodSource("nonForeignKeyDefinitions") + void legacyForeignKeySettersDoNotOverwriteOtherDefinitions(AlterExpression action) { + Index originalIndex = action.getIndex(); + NamedConstraint originalConstraint = action.getConstraint(); + List originalPkColumns = action.getPkColumns(); + List originalUkColumns = action.getUkColumns(); + String before = action.toString(); + assertNoForeignKeyValues(action); + clearForeignKeyValues(action); + assertThrows(IllegalStateException.class, () -> action.setFkColumns(List.of("fk"))); + assertThrows(IllegalStateException.class, () -> action.setFkSourceSchema("schema")); + assertThrows(IllegalStateException.class, () -> action.setFkSourceTable("parent")); + assertThrows(IllegalStateException.class, () -> action.setFkSourceColumns(List.of("id"))); + assertThrows(IllegalStateException.class, + () -> action.setReferentialAction(Type.DELETE, Action.CASCADE)); + assertSame(originalIndex, action.getIndex()); + assertSame(originalConstraint, action.getConstraint()); + assertEquals(originalPkColumns, action.getPkColumns()); + assertEquals(originalUkColumns, action.getUkColumns()); + assertEquals(before, action.toString()); + } + + private static void assertNoForeignKeyValues(AlterExpression action) { + assertNull(action.getFkColumns()); + assertNull(action.getFkSourceSchema()); + assertNull(action.getFkSourceTable()); + assertNull(action.getFkSourceColumns()); + assertNull(action.getReferentialAction(Type.DELETE)); + assertFalse(action.isOnDeleteCascade()); + assertFalse(action.isOnDeleteRestrict()); + assertFalse(action.isOnDeleteSetNull()); + } + + private static void clearForeignKeyValues(AlterExpression action) { + action.setFkColumns(null); + action.setFkSourceSchema(null); + action.setFkSourceTable(null); + action.setFkSourceColumns(null); + action.removeReferentialAction(Type.DELETE); + action.setOnDeleteCascade(false); + action.setOnDeleteRestrict(false); + action.setOnDeleteSetNull(false); + } + + private static ForeignKeyConstraint foreignKey(AlterExpression action) { + return assertInstanceOf(ForeignKeyConstraint.class, action.getConstraint()); + } + + private static AlterExpression first(Alter statement) { + return statement.getAlterExpressions().get(0); + } + + private static Alter statement(AlterExpression action) { + Alter statement = new Alter(); + statement.setTable(new Table("child")); + statement.addAlterExpression(action); + return statement; + } + + private static Alter parse(String sql, Dialect dialect) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static void assertRoundTrip(String expected, Alter statement, Dialect dialect) + throws JSQLParserException { + assertEquals(expected, statement.toString()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(expected, output.toString()); + assertEquals(expected, parse(output.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterKeyAccessorsTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterKeyAccessorsTest.java new file mode 100644 index 0000000000..45cb380a91 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterKeyAccessorsTest.java @@ -0,0 +1,171 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.KeyElement; +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterKeyAccessorsTest { + private List columns(AlterExpression action, boolean primary) { + return primary ? action.getPkColumns() : action.getUkColumns(); + } + + private void setColumns(AlterExpression action, boolean primary, List columns) { + if (primary) { + action.setPkColumns(columns); + } else { + action.setUkColumns(columns); + } + } + + private void assertDeparsed(Alter statement, String expected) { + assertEquals(expected, statement.toString()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(expected, output.toString()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void readsCurrentIndexAndAppliesSettersAndFluentAdditions(boolean primary) throws Exception { + String prefix = "ALTER TABLE t ADD " + (primary ? "PRIMARY KEY" : "UNIQUE"); + Alter statement = (Alter) CCJSqlParserUtil.parse(prefix + " (a)"); + AlterExpression action = statement.getAlterExpressions().get(0); + KeyConstraint originalIndex = (KeyConstraint) action.getConstraint(); + originalIndex.setColumnsNames(List.of("b")); + List snapshot = columns(action, primary); + assertEquals(List.of("b"), snapshot); + snapshot.set(0, "c"); + assertEquals(List.of("b"), columns(action, primary)); + assertDeparsed(statement, prefix + " (b)"); + setColumns(action, primary, List.of("c")); + assertDeparsed(statement, prefix + " (c)"); + if (primary) { + action.addPkColumns("d").addPkColumns(List.of("e")); + } else { + action.addUkColumns("d").addUkColumns(List.of("e")); + } + assertEquals(List.of("c", "d", "e"), columns(action, primary)); + assertSame(originalIndex, action.getConstraint()); + assertDeparsed(statement, prefix + " (c, d, e)"); + setColumns(action, primary, null); + assertTrue(columns(action, primary).isEmpty()); + assertTrue(originalIndex.getColumns().isEmpty()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void replacingWithBareNamesDropsElementOptionsButKeepsIndexMetadata(boolean primary) + throws Exception { + String prefix = "ALTER TABLE t ADD CONSTRAINT key_t " + + (primary ? "PRIMARY KEY" : "UNIQUE"); + Alter statement = (Alter) CCJSqlParserUtil.parse(prefix + " (a DESC, b) DEFERRABLE"); + AlterExpression action = statement.getAlterExpressions().get(0); + KeyConstraint index = (KeyConstraint) action.getConstraint(); + KeyElement first = index.getColumns().get(0); + assertEquals("a DESC", first.toString()); + setColumns(action, primary, List.of("b", "a")); + assertEquals(List.of("b", "a"), columns(action, primary)); + assertNull(index.getColumns().get(1).getSortOrder()); + assertNull(index.getColumns().get(1).getParams()); + assertNotSame(first, index.getColumns().get(1)); + assertSame(index, action.getConstraint()); + assertEquals("key_t", index.getName()); + assertDeparsed(statement, prefix + " (b, a) DEFERRABLE"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void settingAnUnchangedSnapshotPreservesSqlButRebuildsPlainElements(boolean primary) + throws Exception { + String prefix = "ALTER TABLE t ADD " + (primary ? "PRIMARY KEY" : "UNIQUE"); + Alter statement = (Alter) CCJSqlParserUtil.parse(prefix + " (a DESC, b)"); + AlterExpression action = statement.getAlterExpressions().get(0); + KeyConstraint index = (KeyConstraint) action.getConstraint(); + KeyElement expression = new KeyElement(new Column("a")) + .withExpressionParenthesized(false) + .withSortOrder(KeyElement.SortOrder.DESC); + index.getColumns().set(0, expression); + setColumns(action, primary, new ArrayList<>(columns(action, primary))); + KeyElement replacement = index.getColumns().get(0); + assertNotSame(expression, replacement); + assertEquals("a DESC", replacement.getColumnName()); + assertNull(replacement.getExpression()); + assertNull(replacement.getSortOrder()); + assertDeparsed(statement, prefix + " (a DESC, b)"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void structuredReorderingRetainsElementIdentityAndOptions(boolean primary) throws Exception { + String prefix = "ALTER TABLE t ADD " + (primary ? "PRIMARY KEY" : "UNIQUE"); + Alter statement = (Alter) CCJSqlParserUtil.parse(prefix + " (a DESC, b)"); + AlterExpression action = statement.getAlterExpressions().get(0); + KeyConstraint index = (KeyConstraint) action.getConstraint(); + KeyElement first = index.getColumns().get(0); + KeyElement second = index.getColumns().get(1); + index.setColumns(List.of(second, first)); + assertSame(first, index.getColumns().get(1)); + assertSame(second, index.getColumns().get(0)); + assertEquals(KeyElement.SortOrder.DESC, first.getSortOrder()); + assertEquals(List.of("b", "a DESC"), columns(action, primary)); + assertDeparsed(statement, prefix + " (b, a DESC)"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void supportsAnIndexWithoutColumnsAndFluentPopulation(boolean primary) { + KeyConstraint index = new KeyConstraint().withType(primary ? "PRIMARY KEY" : "UNIQUE"); + AlterExpression action = new AlterExpression().withOperation(AlterOperation.ADD) + .withConstraint(index); + assertTrue(index.getColumnsNames().isEmpty()); + assertTrue(columns(action, primary).isEmpty()); + index.getColumnsNames().add("detached"); + assertNull(index.getColumns()); + if (primary) { + action.addPkColumns("a"); + } else { + action.addUkColumns("a"); + } + assertEquals(List.of("a"), columns(action, primary)); + index.setColumns(null); + assertTrue(columns(action, primary).isEmpty()); + setColumns(action, primary, List.of("b")); + assertEquals(List.of("b"), index.getColumnsNames()); + } + + @Test + void legacyConstructionUsesASingleDefinitionAndRejectsUnrelatedIndexes() { + AlterExpression action = new AlterExpression().withOperation(AlterOperation.ADD) + .withPkColumns(new ArrayList<>(List.of("a"))); + action.getPkColumns().add("b"); + assertEquals("ADD PRIMARY KEY (a)", action.toString()); + action.addPkColumns("b"); + assertEquals("ADD PRIMARY KEY (a, b)", action.toString()); + assertSame(action.getConstraint(), action.getKeyConstraint()); + Index unrelated = new Index().withType("INDEX").withColumnsNames(List.of("other")); + action.setIndex(unrelated); + assertThrows(IllegalStateException.class, () -> action.setPkColumns(List.of("pk"))); + assertThrows(IllegalStateException.class, () -> action.setUkColumns(List.of("uk"))); + assertNull(action.getPkColumns()); + assertNull(action.getUkColumns()); + assertEquals(List.of("other"), unrelated.getColumnsNames()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterKeyStateTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterKeyStateTest.java new file mode 100644 index 0000000000..176a5569a6 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterKeyStateTest.java @@ -0,0 +1,293 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.ConstraintUsingIndex; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.KeyElement; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterKeyStateTest { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void legacyBuildersCreateOneKeyAndExposeOnlySnapshots(boolean primary) throws Exception { + AlterExpression action = new AlterExpression(); + assertNull(action.getKeyConstraint()); + assertNull(action.getPkColumns()); + assertNull(action.getUkColumns()); + setColumns(action, primary, null); + action.setUkName(null); + action.setUkTypeSpecified(false); + assertNull(action.getConstraint()); + + List input = new ArrayList<>(List.of("\"key.name\"")); + setColumns(action, primary, input); + input.add("detached"); + KeyConstraint key = action.getKeyConstraint(); + assertSame(key, action.getConstraint()); + assertEquals(primary ? ConstraintKind.PRIMARY_KEY : ConstraintKind.UNIQUE, key.getKind()); + columns(action, primary).add("also_detached"); + assertEquals(List.of("\"key.name\""), key.getColumnsNames()); + assertNull(action.getOperation()); + action.setOperation(AlterOperation.ADD); + roundTrip(statement(action), Dialect.POSTGRESQL); + setColumns(action, primary, List.of("other")); + assertSame(key, action.getKeyConstraint()); + assertEquals(List.of("other"), key.getColumnsNames()); + roundTrip(statement(action), Dialect.POSTGRESQL); + setColumns(action, primary, null); + assertSame(key, action.getKeyConstraint()); + assertTrue(key.getColumns().isEmpty()); + } + + @Test + void parsedPrimaryKeyPreservesAttributesAndUsesTheSameEditableElements() throws Exception { + Alter statement = parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) " + + "INCLUDE (payload) DEFERRABLE", Dialect.POSTGRESQL); + AlterExpression action = first(statement); + KeyConstraint key = action.getKeyConstraint(); + assertEquals(Boolean.TRUE, key.getConstraintAttributes().getDeferrable()); + assertEquals(List.of("payload"), key.getIncludeColumns()); + action.setPkColumns(List.of("\"new.id\"")); + assertEquals(List.of("\"new.id\""), key.getColumnsNames()); + KeyElement replacement = new KeyElement("final_id"); + key.setColumns(List.of(replacement)); + assertSame(replacement, action.getKeyConstraint().getColumns().get(0)); + assertEquals(List.of("final_id"), action.getPkColumns()); + assertEquals("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (final_id) " + + "INCLUDE (payload) DEFERRABLE", statement.toString()); + roundTrip(statement, Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = {"UNIQUE", "UNIQUE KEY", "UNIQUE INDEX"}) + void uniqueNameAndKeywordAliasesFollowTheStructuredDefinition(String type) throws Exception { + Alter statement = parse("ALTER TABLE t ADD " + type + " uq (id)", Dialect.MYSQL); + AlterExpression action = first(statement); + KeyConstraint key = action.getKeyConstraint(); + assertEquals(type.endsWith("KEY"), action.getUk()); + assertEquals(!"UNIQUE".equals(type), action.isUkTypeSpecified()); + key.setIndexName("typed_name"); + assertEquals("typed_name", action.getUkName()); + action.setUkName("legacy_name"); + assertEquals("legacy_name", key.getIndexName()); + action.setUk(true); + assertTrue(action.getUk()); + assertTrue(action.isUkTypeSpecified()); + assertEquals("ALTER TABLE t ADD UNIQUE KEY legacy_name (id)", statement.toString()); + roundTrip(statement, Dialect.MYSQL); + action.setUk(false); + assertFalse(action.getUk()); + assertEquals("ALTER TABLE t ADD UNIQUE INDEX legacy_name (id)", statement.toString()); + roundTrip(statement, Dialect.MYSQL); + action.setUkTypeSpecified(false); + assertFalse(action.isUkTypeSpecified()); + assertEquals("ALTER TABLE t ADD UNIQUE legacy_name (id)", statement.toString()); + roundTrip(statement, Dialect.MYSQL); + action.setUkTypeSpecified(true); + assertFalse(action.getUk()); + assertTrue(action.isUkTypeSpecified()); + roundTrip(statement, Dialect.MYSQL); + } + + @Test + void genericNamedUniqueKeywordEditsKeepTheLegacyHeaderInSync() throws Exception { + Alter statement = parse("ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uq (id)", null); + AlterExpression action = first(statement); + action.setUk(false); + assertEquals("UNIQUE INDEX", action.getConstraintType()); + assertFalse(action.getUk()); + assertTrue(action.isUkTypeSpecified()); + roundTrip(statement, null); + action.setUkTypeSpecified(false); + assertEquals("UNIQUE", action.getConstraintType()); + assertFalse(action.isUkTypeSpecified()); + roundTrip(statement, null); + } + + @Test + void replacementsAndClearsCannotRestoreOldKeyNamesOrKeywords() { + AlterExpression action = new AlterExpression().withUkName("old_name").withUk(true) + .withUkColumns(List.of("old_column")); + KeyConstraint old = action.getKeyConstraint(); + KeyConstraint replacement = new KeyConstraint().withType("UNIQUE") + .withColumnsNames(List.of("new_column")); + action.setConstraint(replacement); + assertNull(action.getUkName()); + assertFalse(action.getUk()); + assertFalse(action.isUkTypeSpecified()); + assertEquals(List.of("new_column"), action.getUkColumns()); + assertEquals("old_name", old.getIndexName()); + action.setConstraint(null); + assertNull(action.getKeyConstraint()); + assertNull(action.getUkColumns()); + assertNull(action.getUkName()); + action.setUkName("fresh"); + assertNotSame(old, action.getKeyConstraint()); + assertNotSame(replacement, action.getKeyConstraint()); + assertNull(action.getKeyConstraint().getColumns()); + assertFalse(action.isUkTypeSpecified()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void settersRejectCompetingDefinitionsAndNullClearsLeaveThemIntact(boolean primary) { + List competitors = List.of( + new KeyConstraint().withType(primary ? "UNIQUE" : "PRIMARY KEY"), + new CheckConstraint(), new ForeignKeyConstraint(), new ConstraintUsingIndex()); + for (NamedConstraint competitor : competitors) { + AlterExpression action = new AlterExpression().withConstraint(competitor); + setColumns(action, primary, null); + assertSame(competitor, action.getConstraint()); + assertThrows(IllegalStateException.class, + () -> setColumns(action, primary, List.of("id"))); + assertSame(competitor, action.getConstraint()); + } + AlterExpression action = new AlterExpression(); + Index index = new Index().withType("INDEX").withName("ix"); + action.setIndex(index); + setColumns(action, primary, null); + assertThrows(IllegalStateException.class, + () -> setColumns(action, primary, List.of("id"))); + assertSame(index, action.getIndex()); + assertNull(action.getKeyConstraint()); + action.setIndex(null); + action.setFkColumns(List.of("fk_column")); + assertThrows(IllegalStateException.class, + () -> setColumns(action, primary, List.of("id"))); + assertEquals(List.of("fk_column"), action.getFkColumns()); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP (\"a.b\", c) CASCADE CONSTRAINTS", + "DROP UNIQUE (\"a.b\", c)", "DROP FOREIGN KEY (\"a.b\", c)"}) + void parsedDropTargetsAreOwnedAndSeparateFromKeyDefinitions(String body) throws Exception { + Alter statement = parse("ALTER TABLE t " + body, null); + AlterExpression action = first(statement); + assertNull(action.getConstraint()); + assertNull(action.getKeyConstraint()); + assertEquals(List.of("\"a.b\"", "c"), action.getDropColumns()); + List snapshot = action.getPkColumns(); + snapshot.set(0, "detached"); + assertEquals("\"a.b\"", action.getDropColumns().get(0)); + action.getDropColumns().set(0, "\"new.name\""); + assertEquals(List.of("\"new.name\"", "c"), action.getPkColumns()); + roundTrip(statement, null); + List replacement = new ArrayList<>(List.of("other")); + action.setPkColumns(replacement); + replacement.add("detached"); + assertEquals(List.of("other"), action.getDropColumns()); + action.addDropColumns("next").addDropColumns(List.of("last")); + assertEquals(List.of("other", "next", "last"), action.getDropColumns()); + roundTrip(statement, null); + action.setPkColumns(null); + assertNull(action.getDropColumns()); + assertNull(action.getConstraint()); + } + + @ParameterizedTest + @EnumSource(value = AlterOperation.class, names = {"DROP", "DROP_UNIQUE", "DROP_FOREIGN_KEY"}) + void newDropBuilderDoesNotDependOnSetterOrder(AlterOperation operation) throws Exception { + AlterExpression action = new AlterExpression().withDropColumns(List.of("\"a.b\"")); + assertNull(action.getConstraint()); + assertNull(action.getPkColumns()); + action.setOperation(operation); + assertEquals(List.of("\"a.b\""), action.getPkColumns()); + roundTrip(statement(action), null); + } + + @Test + void changingOperationDoesNotConvertDefinitionsIntoDropTargets() throws Exception { + AlterExpression action = new AlterExpression().withPkColumns(List.of("id")) + .withDropColumns(List.of("old_id")).withOperation(AlterOperation.ADD); + KeyConstraint key = action.getKeyConstraint(); + assertEquals("ADD PRIMARY KEY (id)", action.toString()); + action.setOperation(AlterOperation.DROP); + assertEquals("DROP (old_id)", action.toString()); + assertEquals(List.of("old_id"), action.getPkColumns()); + action.setPkColumns(List.of("replacement")); + assertEquals(List.of("id"), key.getColumnsNames()); + assertEquals("DROP (replacement)", action.toString()); + action.setOperation(AlterOperation.ADD); + assertEquals("ADD PRIMARY KEY (id)", action.toString()); + assertEquals(List.of("replacement"), action.getDropColumns()); + roundTrip(statement(action), Dialect.POSTGRESQL); + action.setDropColumns(null); + assertSame(key, action.getKeyConstraint()); + assertNull(action.getDropColumns()); + } + + @Test + void namedAndPrimaryKeyDropsDoNotInventColumnTargets() throws Exception { + Alter statement = + parse("ALTER TABLE t DROP FOREIGN KEY fk, DROP PRIMARY KEY", Dialect.MYSQL); + for (AlterExpression action : statement.getAlterExpressions()) { + assertNull(action.getDropColumns()); + assertNull(action.getPkColumns()); + assertNull(action.getKeyConstraint()); + } + roundTrip(statement, Dialect.MYSQL); + } + + private static List columns(AlterExpression action, boolean primary) { + return primary ? action.getPkColumns() : action.getUkColumns(); + } + + private static void setColumns(AlterExpression action, boolean primary, List columns) { + if (primary) { + action.setPkColumns(columns); + } else { + action.setUkColumns(columns); + } + } + + private static Alter parse(String sql, Dialect dialect) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + } + + private static AlterExpression first(Alter statement) { + return statement.getAlterExpressions().get(0); + } + + private static Alter statement(AlterExpression action) { + Alter statement = new Alter(); + statement.setTable(new Table("t")); + statement.addAlterExpression(action); + return statement; + } + + private static void roundTrip(Alter statement, Dialect dialect) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql)); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterPartitionMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterPartitionMutationTest.java new file mode 100644 index 0000000000..37319554d4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterPartitionMutationTest.java @@ -0,0 +1,238 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; + +class AlterPartitionMutationTest { + + @Test + void legacyExpressionSetterChangesParsedPartitioning() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY HASH (id) PARTITIONS 8"); + AlterExpression legacy = action(alter); + Column replacement = new Column("other"); + legacy.setPartitionExpression(replacement); + + assertSame(replacement, action(alter).getPartitioning().getExpression()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY HASH (other) PARTITIONS 8"); + } + + @Test + void legacyColumnsReplaceExpressionsWithAppropriateSyntax() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY LINEAR HASH (id) PARTITIONS 8"); + AlterExpression legacy = action(alter); + legacy.setPartitionType("key"); + legacy.setPartitionColumns(List.of("other")); + + assertNull(legacy.getPartitionExpression()); + assertFalse(action(alter).getPartitioning().isColumnsSyntax()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY LINEAR KEY (other) PARTITIONS 8"); + } + + @Test + void legacyRangeColumnsAndExpressionSettersReplaceOneAnother() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (100), PARTITION pmax VALUES LESS THAN MAXVALUE)"); + AlterExpression legacy = action(alter); + legacy.setPartitionColumns(List.of("other")); + + assertNull(legacy.getPartitionExpression()); + assertTrue(action(alter).getPartitioning().isColumnsSyntax()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE COLUMNS (other) " + + "(PARTITION p0 VALUES LESS THAN (100), PARTITION pmax VALUES LESS THAN MAXVALUE)"); + + legacy.setPartitionExpression(new Column("id")); + assertNull(legacy.getPartitionColumns()); + assertFalse(action(alter).getPartitioning().isColumnsSyntax()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (100), PARTITION pmax VALUES LESS THAN MAXVALUE)"); + } + + @Test + void legacyColumnNamesAreAMutableViewOfStructuredColumns() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY KEY (`id`, other) PARTITIONS 8"); + AlterExpressionPartition action = action(alter); + List names = action.getPartitionColumns(); + assertEquals("`id`", names.set(0, "`new_id`")); + names.add("extra"); + assertEquals("other", names.remove(1)); + action.getPartitioning().getColumns().get(1).setColumnName("last_id"); + + assertEquals(List.of("`new_id`", "last_id"), names); + assertRoundTrip(alter, + "ALTER TABLE sales PARTITION BY KEY (`new_id`, last_id) PARTITIONS 8"); + names.clear(); + assertTrue(action.getPartitioning().getColumns().isEmpty()); + names.add("id"); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY KEY (id) PARTITIONS 8"); + } + + @Test + void structuredMutationsAreVisibleThroughLegacyAccessors() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY HASH (id) PARTITIONS 8"); + AlterExpressionPartition action = action(alter); + TablePartitioning partitioning = action.getPartitioning(); + partitioning.setType(TablePartitioning.Type.KEY); + partitioning.setColumns(new ExpressionList<>(new Column("other"))); + + assertEquals("KEY", action.getPartitionType()); + assertEquals(List.of("other"), action.getPartitionColumns()); + assertNull(action.getPartitionExpression()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY KEY (other) PARTITIONS 8"); + + partitioning.setType(TablePartitioning.Type.HASH); + Column id = new Column("id"); + partitioning.setExpression(id); + assertSame(id, action.getPartitionExpression()); + assertNull(action.getPartitionColumns()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY HASH (id) PARTITIONS 8"); + } + + @Test + void partitionDefinitionsShareTheStructuredList() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (100))"); + AlterExpressionPartition action = action(alter); + List replacements = new ArrayList<>(action(parse( + "ALTER TABLE sales ADD PARTITION (PARTITION p1 VALUES LESS THAN (200))")) + .getPartitionDefinitions()); + action.setPartitionDefinitions(replacements); + assertSame(replacements, action.getPartitioning().getPartitionDefinitions()); + action.addPartitionDefinitions(action(parse("ALTER TABLE sales ADD PARTITION " + + "(PARTITION pmax VALUES LESS THAN MAXVALUE)")).getPartitionDefinitions()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p1 VALUES LESS THAN (200), PARTITION pmax VALUES LESS THAN MAXVALUE)"); + + action.getPartitioning().setPartitionDefinitions(null); + assertNull(action.getPartitionDefinitions()); + action.setPartitionDefinitions(replacements); + action.getPartitionDefinitions().remove(0); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION pmax VALUES LESS THAN MAXVALUE)"); + } + + @Test + void reorganizeUsesTheSameDefinitionsAsTheStructuredClause() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (100))"); + AlterExpressionPartition action = action(alter); + action.setOperation(AlterOperation.REORGANIZE_PARTITION); + action.setPartitionNames(List.of("p_old")); + action.getPartitioning().setPartitionDefinitions(action(parse( + "ALTER TABLE sales ADD PARTITION (PARTITION p1 VALUES LESS THAN (200))")) + .getPartitionDefinitions()); + assertRoundTrip(alter, "ALTER TABLE sales REORGANIZE PARTITION p_old INTO " + + "(PARTITION p1 VALUES LESS THAN (200))"); + } + + @Test + void clearingLegacyExpressionAlsoClearsAKeyListWithoutResurrectingOldKeys() { + AlterExpressionPartition action = new AlterExpressionPartition() + .withPartitioning(new TablePartitioning(TablePartitioning.Type.RANGE)); + ExpressionList keys = + new ExpressionList<>(new Column("id"), new Column("other")); + action.setPartitionExpression(keys); + assertSame(keys, action.getPartitioning().getExpressionList()); + assertSame(keys, action.getPartitionExpression()); + action.setPartitionColumns(null); + assertSame(keys, action.getPartitionExpression()); + action.setPartitionExpression(null); + assertNull(action.getPartitioning().getExpressionList()); + assertNull(action.getPartitionExpression()); + + action.setPartitionColumns(List.of("id")); + action.setPartitionExpression(null); + assertEquals(List.of("id"), action.getPartitionColumns()); + action.setPartitionColumns(null); + assertNull(action.getPartitionColumns()); + } + + @Test + void replacingAndClearingPartitioningDoesNotLeaveLegacySnapshots() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY RANGE COLUMNS (id) " + + "(PARTITION p0 VALUES LESS THAN (100))"); + AlterExpressionPartition action = action(alter); + TablePartitioning replacement = new TablePartitioning(TablePartitioning.Type.HASH) + .withExpression(new Column("other")).withPartitions(4L); + action.setPartitioning(replacement); + assertNull(action.getPartitionColumns()); + assertNull(action.getPartitionDefinitions()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY HASH (other) PARTITIONS 4"); + + action.setPartitioning(null); + assertNull(action.getPartitionType()); + assertNull(action.getPartitionExpression()); + assertNull(action.getPartitionColumns()); + assertNull(action.getPartitionDefinitions()); + action.setPartitionType("RANGE"); + action.setPartitionExpression(new Column("id")); + action.setPartitionDefinitions(action(parse("ALTER TABLE sales ADD PARTITION " + + "(PARTITION pmax VALUES LESS THAN MAXVALUE)")).getPartitionDefinitions()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION pmax VALUES LESS THAN MAXVALUE)"); + } + + @Test + void quotedDotsRemainPartOfTheColumnNameInLegacySettersAndListEdits() + throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY KEY (id) PARTITIONS 8"); + AlterExpressionPartition action = action(alter); + action.setPartitionColumns(List.of("`event.date`")); + Column column = action.getPartitioning().getColumns().get(0); + assertNull(column.getTable()); + assertEquals("`event.date`", column.getColumnName()); + assertRendering(alter, "ALTER TABLE sales PARTITION BY KEY (`event.date`) PARTITIONS 8"); + + List names = action.getPartitionColumns(); + assertEquals("`event.date`", names.set(0, "`renamed.value`")); + names.add("`other.column`"); + assertEquals(List.of("`renamed.value`", "`other.column`"), names); + action.getPartitioning().getColumns().forEach(item -> assertNull(item.getTable())); + assertRendering(alter, "ALTER TABLE sales PARTITION BY KEY " + + "(`renamed.value`, `other.column`) PARTITIONS 8"); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static AlterExpressionPartition action(Alter alter) { + return (AlterExpressionPartition) alter.getAlterExpressions().get(0); + } + + private static void assertRoundTrip(Alter alter, String expected) throws JSQLParserException { + assertRendering(alter, expected); + assertEquals(expected, parse(alter.toString()).toString()); + } + + private static void assertRendering(Alter alter, String expected) { + assertEquals(expected, alter.toString()); + StringBuilder output = new StringBuilder(); + alter.accept(new StatementDeParser(output), null); + assertEquals(expected, output.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterRendererDispatchTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRendererDispatchTest.java new file mode 100644 index 0000000000..26fb12c3c2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRendererDispatchTest.java @@ -0,0 +1,258 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetDefault; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +class AlterRendererDispatchTest { + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "POSTGRESQL"}) + void parsedDefaultsRemainEditableAndUseTheExpressionVisitor(Dialect dialect) + throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN amount SET DEFAULT (1 + 2)", dialect); + AlterExpression action = statement.getAlterExpressions().get(0); + addInactiveColumn(action); + ColumnDefaultAction defaultAction = action.getColumnDefaults().get(0); + assertSame(action.getColumnSetDefaultList().get(0), defaultAction); + defaultAction.setColumnName("renamed"); + defaultAction.setDefaultExpression(CCJSqlParserUtil.parseExpression("(3 + 4)")); + assertFalse(action.hasActiveColumnDefinitions()); + String expected = "ALTER TABLE t ALTER COLUMN renamed SET DEFAULT (3 + 4)"; + assertEquals(expected, statement.toString()); + assertRenderersAgree(statement); + assertEquals(expected, parse(expected, dialect).toString()); + + List visited = new ArrayList<>(); + assertEquals("ALTER TABLE t ALTER COLUMN renamed SET DEFAULT (103 + 104)", + deparse(statement, visited)); + assertEquals(List.of(3L, 4L), visited); + } + + @Test + void dropFallbackKeepsIdentityBodyInsteadOfInactiveDefaultMetadata() { + AlterExpressionDrop action = new AlterExpressionDrop(); + action.setOperation(AlterOperation.ALTER); + action.hasColumn(true); + ColumnDataType column = new ColumnDataType(false).withColumnName("id"); + column.setIdentityAlterations( + List.of(new IdentityAlteration(IdentityAlteration.Kind.RESTART))); + action.addColDataType(column); + action.addColSetDefault(ColumnSetDefault.fromExpression("other", new LongValue(1))); + Alter statement = statement(action); + assertTrue(action.hasActiveColumnDefinitions()); + assertTrue(action.getColumnDefaults().isEmpty()); + assertEquals("ALTER TABLE t ALTER COLUMN id RESTART", statement.toString()); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals(statement.toString(), deparse(statement, visited)); + assertTrue(visited.isEmpty()); + } + + @Test + void dropFallbackStillRewritesItsActiveColumnExpressionAndAppendsTailOnce() + throws JSQLParserException { + AlterExpressionDrop action = new AlterExpressionDrop(); + action.setOperation(AlterOperation.ALTER); + action.hasColumn(true); + ColumnDataType column = new ColumnDataType(true).withColumnName("id") + .withColDataType(new ColDataType("INT")); + column.setUsingExpression(CCJSqlParserUtil.parseExpression("id + 2")); + action.addColDataType(column); + action.addColSetDefault(ColumnSetDefault.fromExpression("other", new LongValue(1))); + action.addParameters("CASCADE"); + Alter statement = statement(action); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals("ALTER TABLE t ALTER COLUMN id TYPE INT USING id + 102 CASCADE", + deparse(statement, visited)); + assertEquals(List.of(2L), visited); + } + + static Stream fixedBodies() { + return Stream.of( + Arguments.of("ALTER TABLE t RENAME TO renamed", Dialect.MYSQL, + AlterExpressionRename.class), + Arguments.of("ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4", Dialect.MYSQL, + AlterExpressionCharset.class), + Arguments.of("ALTER TABLE t ENGINE = InnoDB", Dialect.MYSQL, + AlterExpressionTableOption.class), + Arguments.of("ALTER TABLE t ALTER CONSTRAINT ck DEFERRABLE", Dialect.POSTGRESQL, + AlterConstraintAttributes.class), + Arguments.of("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id)", + Dialect.COCKROACHDB, AlterExpressionPrimaryKey.class), + Arguments.of("ALTER TABLE t DETACH PARTITION p", Dialect.POSTGRESQL, + AlterExpressionPartition.class), + Arguments.of("ALTER TABLE t ORDER BY id", Dialect.MYSQL, + AlterExpressionOrderBy.class), + Arguments.of("ALTER TABLE t OWNER TO alice", Dialect.POSTGRESQL, + RelationAlterAction.class), + Arguments.of("ALTER FOREIGN TABLE t OPTIONS (ADD filename 'file')", + Dialect.POSTGRESQL, AlterForeignDataOptions.class)); + } + + @ParameterizedTest + @MethodSource("fixedBodies") + void specializedBodiesIgnoreUnrelatedDefaultAndColumnMetadata(String sql, Dialect dialect, + Class type) throws JSQLParserException { + Alter statement = parse(sql, dialect); + AlterExpression action = assertInstanceOf(type, statement.getAlterExpressions().get(0)); + String expected = statement.toString(); + action.addColSetDefault(ColumnSetDefault.fromExpression("inactive", new LongValue(99))); + addInactiveColumn(action); + assertTrue(action.getColumnDefaults().isEmpty()); + assertFalse(action.hasActiveColumnDefinitions()); + assertEquals(expected, statement.toString()); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals(expected, deparse(statement, visited)); + assertTrue(visited.isEmpty()); + + // A changed operation does not replace a subclass's own body dispatch. + action.setOperation(AlterOperation.ALTER); + assertTrue(action.getColumnDefaults().isEmpty()); + assertFalse(action.hasActiveColumnDefinitions()); + assertRenderersAgree(statement); + assertEquals(statement.toString(), deparse(statement, visited)); + assertTrue(visited.isEmpty()); + } + + static Stream prioritizedBodies() { + return Stream.of( + body(action -> action + .addColDropDefault(new AlterExpression.ColumnDropDefault("id"))), + body(action -> action.addColSetVisibility( + new AlterExpression.ColumnSetVisibility("id", true))), + body(action -> { + action.setConstraintType("CHECK"); + action.setConstraintSymbol("ck"); + action.setEnforced(true); + }), + body(action -> { + action.setOperation(AlterOperation.RENAME_TABLE); + action.setNewTableName("renamed"); + }), + body(action -> action.setOperation(AlterOperation.DROP_PRIMARY_KEY)), + body(action -> { + action.setOperation(AlterOperation.UNSPECIFIC); + action.setOptionalSpecifier("vendor_action"); + }), + body(action -> { + action.setOperation(AlterOperation.COMMENT); + action.setCommentText("'description'"); + })); + } + + private static Arguments body(Consumer configure) { + return Arguments.of(configure); + } + + @ParameterizedTest + @MethodSource("prioritizedBodies") + void baseBodyPrecedencePreventsInactiveColumnExpressionsFromBeingRendered( + Consumer configure) { + AlterExpression action = new AlterExpression(); + action.setOperation(AlterOperation.ALTER); + configure.accept(action); + Alter statement = statement(action); + String expected = statement.toString(); + addInactiveColumn(action); + assertFalse(action.hasActiveColumnDefinitions()); + assertTrue(action.getColumnDefaults().isEmpty()); + assertEquals(expected, statement.toString()); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals(expected, deparse(statement, visited)); + assertTrue(visited.isEmpty()); + } + + @Test + void activeDefaultKeepsPriorityOverStaleRenameMetadata() { + AlterExpression action = new AlterExpression(); + action.setOperation(AlterOperation.ALTER); + action.hasColumn(true); + action.addColSetDefault(ColumnSetDefault.fromExpression("id", new LongValue(1))); + action.setOldIndex(new Index().withName("old_index")); + action.setIndex(new Index().withName("new_index")); + addInactiveColumn(action); + assertEquals(1, action.getColumnDefaults().size()); + assertFalse(action.hasActiveColumnDefinitions()); + Alter statement = statement(action); + assertEquals("ALTER TABLE t ALTER COLUMN id SET DEFAULT 1", statement.toString()); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals("ALTER TABLE t ALTER COLUMN id SET DEFAULT 101", deparse(statement, visited)); + assertEquals(List.of(1L), visited); + } + + private static void addInactiveColumn(AlterExpression action) { + ColumnDataType column = new ColumnDataType(true).withColumnName("inactive") + .withColDataType(new ColDataType("INT")); + column.setUsingExpression(new LongValue(999)); + action.addColDataType(column); + } + + private static Alter parse(String sql, Dialect dialect) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static Alter statement(AlterExpression action) { + Alter statement = new Alter(); + statement.setTable(new Table("t")); + statement.addAlterExpression(action); + return statement; + } + + private static void assertRenderersAgree(Alter statement) { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + } + + private static String deparse(Alter statement, List visited) { + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + assertNull(context); + visited.add(value.getValue()); + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), sql), null); + return sql.toString(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterRoutineTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRoutineTest.java new file mode 100644 index 0000000000..e0992bae18 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRoutineTest.java @@ -0,0 +1,334 @@ +/*- + * #%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.statement.alter; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.ConfigurationParameterChange; +import net.sf.jsqlparser.statement.RoutineCharacteristic; +import net.sf.jsqlparser.statement.RoutineCharacteristic.Kind; +import net.sf.jsqlparser.statement.RoutineCharacteristic.SecurityClause; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.create.function.CreateFunction; +import net.sf.jsqlparser.statement.create.procedure.CreateProcedure; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterRoutineTest { + + @ParameterizedTest + @ValueSource(strings = {"ALTER FUNCTION f2(int) OWNER TO u1", + "ALTER FUNCTION public.f2(integer, text) OWNER TO CURRENT_ROLE", + "ALTER FUNCTION f2 OWNER TO SESSION_USER", "ALTER FUNCTION f2(int) RENAME TO g2", + "ALTER PROCEDURE p1() SET SCHEMA archive", + "ALTER ROUTINE \"My Func\"(IN a int, VARIADIC b text[]) RENAME TO \"Other\"", + "ALTER FUNCTION f2(int) DEPENDS ON EXTENSION ext", + "ALTER ROUTINE f2(int) NO DEPENDS ON EXTENSION ext", + "ALTER FUNCTION f2(int) SET search_path = public", + "ALTER FUNCTION f2(int) SET search_path TO app, public SET work_mem = '64MB'", + "ALTER FUNCTION f2(int) SET work_mem TO DEFAULT RESET search_path RESET ALL", + "ALTER PROCEDURE p1(int) SET search_path FROM CURRENT", + "ALTER PROCEDURE p1(int) SECURITY DEFINER", + "ALTER PROCEDURE p1 EXTERNAL SECURITY INVOKER", + "ALTER ROUTINE f2(int) IMMUTABLE", "ALTER FUNCTION f2(int) STABLE NOT LEAKPROOF", + "ALTER FUNCTION f2(int) VOLATILE LEAKPROOF STRICT RESTRICT", + "ALTER FUNCTION f2(int) CALLED ON NULL INPUT", + "ALTER FUNCTION f2(int) RETURNS NULL ON NULL INPUT", + "ALTER FUNCTION f2(int) PARALLEL SAFE PARALLEL RESTRICTED PARALLEL UNSAFE", + "ALTER FUNCTION f2(int) COST 100 ROWS 2.5 SUPPORT pg_catalog.my_support", + "ALTER FUNCTION f2(int) COST -1 ROWS +10"}) + void testPostgreSqlForms(String sql) throws JSQLParserException { + assertRoundTrip(sql, Dialect.POSTGRESQL); + assertRoundTrip(sql, null); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER PROCEDURE p1 COMMENT 'x' SQL SECURITY INVOKER", + "ALTER FUNCTION f1 READS SQL DATA", "ALTER FUNCTION db1.f1 LANGUAGE SQL CONTAINS SQL", + "ALTER PROCEDURE `p 1` NO SQL MODIFIES SQL DATA SQL SECURITY DEFINER", + "ALTER FUNCTION f1 COMMENT 'it''s'"}) + void testMySqlForms(String sql) throws JSQLParserException { + assertRoundTrip(sql, Dialect.MYSQL); + assertRoundTrip(sql, Dialect.MARIADB); + assertRoundTrip(sql, null); + } + + @Test + void testPostgreSqlActions() throws JSQLParserException { + AlterRoutine owner = parsePostgreSql("ALTER FUNCTION s.f2(IN a int, text) OWNER TO u1"); + assertEquals(AlterRoutine.RoutineType.FUNCTION, owner.getRoutineType()); + assertEquals(AlterRoutine.Action.OWNER, owner.getAction()); + assertEquals("u1", owner.getOwner()); + RoutineReference routine = owner.getRoutine(); + assertEquals("s.f2", routine.getName()); + assertEquals(2, routine.getArguments().size()); + assertEquals(RoutineReference.Argument.Mode.IN, routine.getArguments().get(0).getMode()); + assertEquals("a", routine.getArguments().get(0).getName()); + assertEquals("text", routine.getArguments().get(1).getDataType().toString()); + assertTrue(owner.getCharacteristics().isEmpty()); + + AlterRoutine rename = parsePostgreSql("ALTER ROUTINE f2 RENAME TO g2"); + assertEquals(AlterRoutine.RoutineType.ROUTINE, rename.getRoutineType()); + assertEquals(AlterRoutine.Action.RENAME, rename.getAction()); + assertEquals("g2", rename.getNewName()); + assertNull(rename.getRoutine().getArguments()); + + AlterRoutine schema = parsePostgreSql("ALTER PROCEDURE p1() SET SCHEMA archive"); + assertEquals(AlterRoutine.RoutineType.PROCEDURE, schema.getRoutineType()); + assertEquals(AlterRoutine.Action.SET_SCHEMA, schema.getAction()); + assertEquals("archive", schema.getSchemaName()); + assertTrue(schema.getRoutine().getArguments().isEmpty()); + + AlterRoutine dependency = + parsePostgreSql("ALTER FUNCTION f2(int) NO DEPENDS ON EXTENSION ext"); + assertEquals(AlterRoutine.Action.DEPENDS_ON_EXTENSION, dependency.getAction()); + assertEquals("ext", dependency.getExtensionName()); + assertTrue(dependency.isNoDependency()); + + // A configuration parameter named "schema" is a SET, not SET SCHEMA. + AlterRoutine parameter = parsePostgreSql("ALTER FUNCTION f2(int) SET schema = 'x'"); + assertEquals(AlterRoutine.Action.CHARACTERISTICS, parameter.getAction()); + assertEquals("schema", parameter.getCharacteristics().get(0).getParameterChange() + .getParameter()); + } + + @Test + void testPostgreSqlCharacteristics() throws JSQLParserException { + AlterRoutine statement = parsePostgreSql("ALTER FUNCTION f2(int) IMMUTABLE STRICT " + + "EXTERNAL SECURITY DEFINER PARALLEL SAFE COST 10 ROWS 5 SUPPORT s.sup " + + "SET search_path TO app, public RESET ALL RESTRICT"); + assertEquals(AlterRoutine.Action.CHARACTERISTICS, statement.getAction()); + assertTrue(statement.isRestrict()); + List characteristics = statement.getCharacteristics(); + assertEquals(List.of(Kind.IMMUTABLE, Kind.STRICT, Kind.SECURITY_DEFINER, + Kind.PARALLEL_SAFE, Kind.COST, Kind.ROWS, Kind.SUPPORT, + Kind.CONFIGURATION_PARAMETER, Kind.CONFIGURATION_PARAMETER), + characteristics.stream().map(RoutineCharacteristic::getKind) + .collect(java.util.stream.Collectors.toList())); + assertNull(characteristics.get(0).getSecurityClause()); + assertEquals(SecurityClause.EXTERNAL_SECURITY, + characteristics.get(2).getSecurityClause()); + assertEquals("10", statement.getCharacteristic(Kind.COST).orElseThrow().getValue()); + assertEquals("5", statement.getCharacteristic(Kind.ROWS).orElseThrow().getValue()); + assertEquals("s.sup", statement.getCharacteristic(Kind.SUPPORT).orElseThrow().getValue()); + + ConfigurationParameterChange set = characteristics.get(7).getParameterChange(); + assertEquals(ConfigurationParameterChange.Action.SET, set.getAction()); + assertEquals("search_path", set.getParameter()); + assertEquals(2, set.getValues().size()); + ConfigurationParameterChange reset = characteristics.get(8).getParameterChange(); + assertEquals(ConfigurationParameterChange.Action.RESET, reset.getAction()); + assertEquals("ALL", reset.getParameter()); + + AlterRoutine plainSecurity = parsePostgreSql("ALTER PROCEDURE p1(int) SECURITY INVOKER"); + RoutineCharacteristic security = plainSecurity.getCharacteristics().get(0); + assertEquals(Kind.SECURITY_INVOKER, security.getKind()); + assertEquals(SecurityClause.SECURITY, security.getSecurityClause()); + assertFalse(plainSecurity.isRestrict()); + } + + @Test + void testMySqlCharacteristics() throws JSQLParserException { + AlterRoutine statement = (AlterRoutine) CCJSqlParserUtil.parse( + "ALTER PROCEDURE db1.p1 COMMENT 'x' LANGUAGE SQL READS SQL DATA SQL SECURITY INVOKER", + parser -> parser.withDialect(Dialect.MYSQL)); + assertEquals(AlterRoutine.RoutineType.PROCEDURE, statement.getRoutineType()); + assertEquals("db1.p1", statement.getRoutine().getName()); + assertNull(statement.getRoutine().getArguments()); + List characteristics = statement.getCharacteristics(); + assertEquals(4, characteristics.size()); + assertEquals(Kind.COMMENT, characteristics.get(0).getKind()); + assertEquals("'x'", characteristics.get(0).getValue()); + assertEquals(Kind.LANGUAGE_SQL, characteristics.get(1).getKind()); + assertEquals(Kind.READS_SQL_DATA, characteristics.get(2).getKind()); + assertEquals(Kind.SECURITY_INVOKER, characteristics.get(3).getKind()); + assertEquals(SecurityClause.SQL_SECURITY, characteristics.get(3).getSecurityClause()); + assertFalse(statement.isRestrict()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', quoteCharacter = '`', value = { + "'it\\'s'|it's", "'it''s'|it's", "\"dq\"|dq", "'a\\\\b'|a\\b", + "'line1\\nline2'|`line1\nline2`", "'tab\\there'|`tab\there`", + "'percent\\% underscore\\_'|percent\\% underscore\\_", + "'한글 설명 😀'|한글 설명 😀"}) + void testMySqlCommentKeepsLiteralAndStoredText(String literal, String stored) + throws JSQLParserException { + String sql = "ALTER PROCEDURE p1 COMMENT " + literal + " SQL SECURITY INVOKER"; + AlterRoutine statement = (AlterRoutine) assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(Dialect.MYSQL)); + RoutineCharacteristic comment = statement.getCharacteristic(Kind.COMMENT).orElseThrow(); + assertEquals(literal, comment.getValue()); + assertEquals(literal, comment.getComment().toString()); + assertEquals(stored, comment.getComment().getNotExcapedValue()); + } + + @Test + void testCommentMutation() throws JSQLParserException { + AlterRoutine statement = (AlterRoutine) CCJSqlParserUtil.parse( + "ALTER FUNCTION f1 COMMENT 'it\\'s' NO SQL", + parser -> parser.withDialect(Dialect.MYSQL)); + RoutineCharacteristic comment = statement.getCharacteristics().get(0); + assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, comment.getComment().getEscaping()); + + comment.getComment().setValue("changed"); + assertEquals("ALTER FUNCTION f1 COMMENT 'changed' NO SQL", statement.toString()); + comment.setValue("'x''y'"); + assertEquals("x'y", comment.getComment().getNotExcapedValue()); + comment.setComment(new StringValue("plain")); + assertEquals("'plain'", comment.getValue()); + + RoutineCharacteristic created = RoutineCharacteristic.comment(new StringValue("note")); + assertEquals(Kind.COMMENT, created.getKind()); + assertEquals("COMMENT 'note'", created.toString()); + assertEquals("COMMENT 'v'", new RoutineCharacteristic(Kind.COMMENT, "'v'").toString()); + assertNull(new RoutineCharacteristic(Kind.COST, "5").getComment()); + } + + @Test + void testMutationAndActionSwitch() throws JSQLParserException { + AlterRoutine statement = parsePostgreSql("ALTER FUNCTION f2(int) RENAME TO g2"); + statement.setAction(AlterRoutine.Action.OWNER); + statement.setOwner("CURRENT_USER"); + assertEquals("ALTER FUNCTION f2(int) OWNER TO CURRENT_USER", statement.toString()); + statement.setAction(AlterRoutine.Action.RENAME); + assertEquals("ALTER FUNCTION f2(int) RENAME TO g2", statement.toString()); + + statement.setAction(AlterRoutine.Action.CHARACTERISTICS); + statement.setRoutineType(AlterRoutine.RoutineType.ROUTINE); + statement.getCharacteristics().add(new RoutineCharacteristic(Kind.SECURITY_DEFINER)); + statement.getCharacteristics().add(new RoutineCharacteristic(Kind.COST, "5")); + statement.getCharacteristics().add(new RoutineCharacteristic( + new ConfigurationParameterChange(ConfigurationParameterChange.Action.SET, "a") + .withValues(new ExpressionList<>(new StringValue("b"))))); + statement.setRestrict(true); + assertEquals("ALTER ROUTINE f2(int) SECURITY DEFINER COST 5 SET a TO 'b' RESTRICT", + statement.toString()); + + RoutineCharacteristic security = statement.getCharacteristics().get(0); + security.setSecurityClause(SecurityClause.SQL_SECURITY); + security.setKind(Kind.SECURITY_INVOKER); + assertEquals("SQL SECURITY INVOKER", security.toString()); + + RoutineCharacteristic missing = new RoutineCharacteristic(Kind.CONFIGURATION_PARAMETER); + assertThrows(IllegalStateException.class, missing::toString); + } + + @Test + void testVisitorsAndDeparserUseExpressions() throws JSQLParserException { + AlterRoutine statement = + parsePostgreSql("ALTER FUNCTION f2(int) SET search_path TO app, public"); + assertEquals("context:f2", statement.accept(new StatementVisitorAdapter() { + @Override + public String visit(AlterRoutine routine, S context) { + return context + ":" + routine.getRoutine().getName(); + } + }, "context")); + assertTrue(new TablesNamesFinder().getTables((Statement) statement).isEmpty()); + + StringBuilder deparsed = new StringBuilder(); + new StatementDeParser(deparsed).visit(statement, null); + assertEquals("ALTER FUNCTION f2(int) SET search_path TO app, public", + deparsed.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER FUNCTION f", "ALTER PROCEDURE p1", + "ALTER FUNCTION f COMPILE", "ALTER FUNCTION f(int) NOT FENCED", + "ALTER FUNCTION f(int) EXTERNAL NAME 'lib'", "ALTER FUNCTION f(int) SET SECURE", + "ALTER FUNCTION IF EXISTS f(int) RENAME TO g", "ALTER PROCEDURE dbo.p AS SELECT 1", + "ALTER FUNCTION dbo.f(@a int) RETURNS int AS BEGIN RETURN 1 END", + "ALTER FUNCTION f(int) PARALLEL FAST", "ALTER FUNCTION f(int) SECURITY OWNER"}) + void testOtherFormsRemainUnsupported(String sql) throws JSQLParserException { + assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql)); + assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER FUNCTION f(int) IMMUTABLE, STRICT", + "ALTER FUNCTION f(int) RENAME TO g RESTRICT", + "ALTER FUNCTION f(int) OWNER TO u IMMUTABLE", + "ALTER FUNCTION f COMMENT 'x' IMMUTABLE", "ALTER FUNCTION f IMMUTABLE COMMENT 'x'", + "ALTER FUNCTION f(int) COMMENT 'x'", "ALTER ROUTINE r COMMENT 'x'", + "ALTER FUNCTION f COMMENT 'x' RESTRICT"}) + void testRejectInvalidCombinations(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.MYSQL))); + } + + @Test + void testSqlServerRoutinesAreUnchanged() throws JSQLParserException { + Consumer sqlServer = parser -> parser.withDialect(Dialect.SQLSERVER); + assertInstanceOf(CreateFunction.class, CCJSqlParserUtil.parse( + "ALTER FUNCTION dbo.f(@a int) RETURNS int AS BEGIN RETURN @a END", sqlServer)); + assertInstanceOf(CreateProcedure.class, + CCJSqlParserUtil.parse("ALTER PROCEDURE dbo.p AS SELECT 1", sqlServer)); + } + + @Test + void testValidation() { + validateNoErrors("ALTER FUNCTION f2(int) OWNER TO u1", 1, DatabaseType.POSTGRESQL); + validateNoErrors("ALTER PROCEDURE p1(int) SET search_path = public", 1, + DatabaseType.POSTGRESQL); + validateNoErrors("ALTER ROUTINE f2(int) IMMUTABLE", 1, DatabaseType.POSTGRESQL); + validateNoErrors("ALTER FUNCTION f1 READS SQL DATA", 1, DatabaseType.MYSQL, + DatabaseType.MARIADB); + validateNoErrors("ALTER PROCEDURE p1 COMMENT 'x'", 1, DatabaseType.MYSQL, + DatabaseType.MARIADB); + assertFalse(new Validation(List.of(DatabaseType.MYSQL), "ALTER ROUTINE f2(int) STABLE") + .validate().isEmpty()); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), + "ALTER FUNCTION f2(int) STABLE").validate().isEmpty()); + assertTrue(new Validation(List.of(new FeaturesAllowed().add(Feature.alterProcedure)), + "ALTER PROCEDURE p1(int) RESET ALL").validate().isEmpty()); + } + + /** Parses, deparses and reparses with the dialect, or without one if it is null. */ + private static void assertRoundTrip(String sql, Dialect dialect) throws JSQLParserException { + Consumer consumer = dialect == null ? parser -> { + } : parser -> parser.withDialect(dialect); + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, consumer); + assertInstanceOf(AlterRoutine.class, statement); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(sql, deparsed.toString()); + assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer).size()); + } + + private static AlterRoutine parsePostgreSql(String sql) throws JSQLParserException { + return (AlterRoutine) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterSchemaTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterSchemaTest.java new file mode 100644 index 0000000000..45a9eaa3c3 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterSchemaTest.java @@ -0,0 +1,98 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterSchemaTest { + @ParameterizedTest + @ValueSource(strings = {"ALTER SCHEMA analytics RENAME TO reporting", + "ALTER SCHEMA \"a.b\" RENAME TO \"next.schema\"", + "ALTER SCHEMA s OWNER TO app", "ALTER SCHEMA s OWNER TO CURRENT_USER", + "ALTER SCHEMA s OWNER TO CURRENT_ROLE", "ALTER SCHEMA s OWNER TO SESSION_USER", + "ALTER SCHEMA \"Case\" OWNER TO \"role.with.dot\""}) + void parsesAndRendersSchemaActions(String sql) throws Exception { + AlterSchema schema = parse(sql); + assertEquals(sql, schema.toString()); + assertTrue(new TablesNamesFinder().getTables(schema).isEmpty()); + roundTrip(schema); + assertTrue(new Validation(List.of(PostgresqlVersion.V14), sql).validate().isEmpty()); + } + + @Test + void supportsAstMutationAndVisitorDispatch() throws Exception { + AlterSchema schema = parse("ALTER SCHEMA s RENAME TO renamed"); + assertEquals(AlterSchema.Action.RENAME, schema.getAction()); + assertEquals("renamed", schema.getNewName()); + schema.setSchemaName("\"schema.name\""); + schema.setNewName("replacement"); + roundTrip(schema); + schema.setAction(AlterSchema.Action.OWNER); + schema.setOwner("CURRENT_ROLE"); + assertEquals("ALTER SCHEMA \"schema.name\" OWNER TO CURRENT_ROLE", schema.toString()); + roundTrip(schema); + Object context = new Object(); + assertEquals("visited", schema.accept(new StatementVisitorAdapter() { + @Override + public String visit(AlterSchema statement, S supplied) { + assertSame(schema, statement); + assertSame(context, supplied); + return "visited"; + } + }, context)); + } + + @Test + void respectsStatementBoundariesAndValidationCapabilities() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + "ALTER SCHEMA s OWNER TO CURRENT_USER; SELECT 1;"); + assertEquals(2, statements.size()); + assertInstanceOf(AlterSchema.class, statements.get(0)); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), + "ALTER SCHEMA s RENAME TO r").validate().isEmpty()); + assertTrue(new Validation(List.of(new FeaturesAllowed().add(Feature.alterSchema)), + "ALTER SCHEMA s RENAME TO r").validate().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER SCHEMA s RENAME", "ALTER SCHEMA s OWNER TO", + "ALTER SCHEMA s RENAME TO 'not an identifier'", "ALTER SCHEMA s OWNER TO a.b", + "ALTER SCHEMA s OWNER TO a, b", "ALTER SCHEMA a.b RENAME TO c"}) + void rejectsIncompleteActionsAndQualifiedSchemaOrRoleNames(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static AlterSchema parse(String sql) throws JSQLParserException { + return (AlterSchema) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(AlterSchema schema) throws Exception { + StringBuilder out = new StringBuilder(); + schema.accept(new StatementDeParser(out), null); + assertEquals(schema.toString(), out.toString()); + assertEquals(schema.toString(), parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index 5ba1a87625..6472bcb306 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -28,6 +28,7 @@ import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.ReferentialAction; @@ -37,11 +38,13 @@ import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.table.CheckConstraint; -import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; import net.sf.jsqlparser.statement.create.table.Index; -import net.sf.jsqlparser.statement.create.table.Index.ColumnParams; -import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.IndexSpecOption; +import net.sf.jsqlparser.statement.create.table.KeyElement; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.PartitionBound; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -94,6 +97,49 @@ public void testAlterTableAddColumn_ColumnKeyWordImplicit() throws JSQLParserExc assertEquals("varchar (255)", colDataTypes.get(0).getColDataType().toString()); } + @Test + public void testAlterTableAddColumnListTSql() throws JSQLParserException { + // T-SQL and Snowflake let one ADD list several columns without repeating ADD + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD a int, b varchar(10) NOT NULL, c int", + parser -> parser.withDialect(Dialect.SQLSERVER)); + assertEquals(1, alter.getAlterExpressions().size()); + AlterExpression alterExp = alter.getAlterExpressions().get(0); + assertEquals(AlterOperation.ADD, alterExp.getOperation()); + List colDataTypes = alterExp.getColDataTypeList(); + assertEquals(3, colDataTypes.size()); + assertEquals("a", colDataTypes.get(0).getColumnName()); + assertEquals("b", colDataTypes.get(1).getColumnName()); + assertEquals("varchar (10)", colDataTypes.get(1).getColDataType().toString()); + assertEquals("c", colDataTypes.get(2).getColumnName()); + assertDeparse(alter, "ALTER TABLE t ADD a int, b varchar (10) NOT NULL, c int"); + } + + @Test + public void testAlterTableAddColumnListWithConstraintTSql() throws JSQLParserException { + // T-SQL mixes column definitions and table constraints in one ADD list + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD a int, CONSTRAINT ck CHECK (a > 0)", + parser -> parser.withDialect(Dialect.SQLSERVER)); + assertEquals(1, alter.getAlterExpressions().size()); + AlterExpression alterExp = alter.getAlterExpressions().get(0); + assertEquals(AlterOperation.ADD, alterExp.getOperation()); + assertEquals(1, alterExp.getColDataTypeList().size()); + assertNotNull(alterExp.getConstraint()); + } + + @Test + public void testAlterTableAddColumnListSnowflake() throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD COLUMN a int, b int", + parser -> parser.withDialect(Dialect.SNOWFLAKE)); + assertEquals(1, alter.getAlterExpressions().size()); + List colDataTypes = + alter.getAlterExpressions().get(0).getColDataTypeList(); + assertEquals(2, colDataTypes.size()); + assertDeparse(alter, "ALTER TABLE t ADD COLUMN a int, b int"); + } + @Test public void testAlterTableBackBrackets() throws JSQLParserException { String sql = "ALTER TABLE tablename add column (field string comment 'aaaaa')"; @@ -286,7 +332,8 @@ public void testAlterTablePK() throws JSQLParserException { AlterExpression alterExpression = ((Alter) stmt).getAlterExpressions().get(0); assertNull(alterExpression.getConstraintName()); // TODO: should this pass? ==> assertEquals(alterExpression.getPkColumns().get(0), "ID"); - assertEquals(alterExpression.getIndex().getColumnsNames().get(0), "`ID`"); + assertEquals(((KeyConstraint) alterExpression.getConstraint()).getColumnsNames().get(0), + "`ID`"); } @Test @@ -309,7 +356,7 @@ public void testAlterTableCheckConstraint() throws JSQLParserException { Alter created = new Alter().withTable(new Table("`Author`")) .addAlterExpressions(Collections.singleton( new AlterExpression().withOperation(AlterOperation.ADD) - .withIndex(new CheckConstraint() + .withConstraint(new CheckConstraint() .withName("name_not_empty") .withExpression(new NotEqualsTo() .withLeftExpression(new Column("`NAME`")) @@ -368,12 +415,12 @@ public void testAlterTableAddColumn5() throws JSQLParserException { public void testAlterTableAddColumn6() throws JSQLParserException { final String sql = "ALTER TABLE mytable ADD COLUMN col1 timestamp (3) not null"; Statement stmt = CCJSqlParserUtil.parse(sql); - assertStatementCanBeDeparsedAs(stmt, sql); + assertStatementCanBeDeparsedAs(stmt, sql.replace("not null", "NOT NULL")); Alter alter = (Alter) stmt; List alterExps = alter.getAlterExpressions(); AlterExpression col1Exp = alterExps.get(0); - assertEquals("not", col1Exp.getColDataTypeList().get(0).getColumnSpecs().get(0)); - assertEquals("null", col1Exp.getColDataTypeList().get(0).getColumnSpecs().get(1)); + assertEquals("NOT", col1Exp.getColDataTypeList().get(0).getColumnSpecs().get(0)); + assertEquals("NULL", col1Exp.getColDataTypeList().get(0).getColumnSpecs().get(1)); assertTrue(col1Exp.hasColumn()); } @@ -598,6 +645,28 @@ public void testAlterTableDropAndAddUniqueIndexWithAscendingColumns() throws Exc result.toString()); } + @Test + public void testAlterTableAddUniqueIndexWithCommentIssue2503() throws Exception { + String sql = "ALTER TABLE `wxp_dm`.`xqgl_req_report` " + + "ADD UNIQUE INDEX `index2` (`report_name` ASC) USING BTREE COMMENT '唯一索引'"; + + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed(sql); + AlterExpression alterExpression = alter.getAlterExpressions().get(0); + KeyConstraint index = (KeyConstraint) alterExpression.getConstraint(); + + assertNotNull(index); + assertEquals("UNIQUE", index.getType()); + assertEquals("INDEX", index.getIndexKeyword()); + assertEquals("`index2`", index.getIndexName()); + assertEquals(List.of("`report_name` ASC"), index.getColumnsNames()); + assertEquals("'唯一索引'", index.getCommentText()); + + // Keep the legacy fields populated for existing consumers. + assertEquals("`index2`", alterExpression.getUkName()); + assertEquals(List.of("`report_name` ASC"), alterExpression.getUkColumns()); + assertEquals(List.of("USING", "BTREE"), alterExpression.getParameters()); + } + @Test public void testIssue259() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed( @@ -613,7 +682,7 @@ public void testIssue633_2() throws JSQLParserException { .withTable(new Table("american_football_action_plays")) .withIndex( new Index().withName("idx_american_football_action_plays_1") - .addColumns(new ColumnParams("play_type", null)) + .addColumns(new KeyElement("play_type", null)) .withUsing("btree")); assertDeparse(created, statement); assertEqualsObjectTree(parsed, created); @@ -627,11 +696,11 @@ public void testAlterOnlyIssue928() throws JSQLParserException { Alter created = new Alter().withUseOnly(true).withTable(new Table("categories")) .addAlterExpressions( new AlterExpression().withOperation(AlterOperation.ADD) - .withIndex(new NamedConstraint() + .withConstraint(new KeyConstraint() .withName(Collections.singletonList( "pk_categories")) .withType("PRIMARY KEY") - .addColumns(new ColumnParams("category_id")))); + .addColumns(new KeyElement("category_id")))); assertDeparse(created, statement); assertEqualsObjectTree(parsed, created); } @@ -644,8 +713,32 @@ public void testAlterConstraintWithoutFKSourceColumnsIssue929() throws JSQLParse @Test public void testAlterTableAlterColumnDropNotNullIssue918() throws JSQLParserException { - assertSqlCanBeParsedAndDeparsed( + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( "ALTER TABLE \"user_table_t\" ALTER COLUMN name DROP NOT NULL"); + AlterExpression expression = alter.getAlterExpressions().get(0); + + assertNull(expression.getColDataTypeList()); + assertEquals("name", expression.getColumnDropNotNullList().get(0).getColumnName()); + assertTrue(expression.getColumnDropNotNullList().get(0).isWithNot()); + } + + @Test + public void testAlterTableAlterColumnSetNotNull() throws JSQLParserException { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE user_table ALTER COLUMN name SET NOT NULL"); + AlterExpression expression = alter.getAlterExpressions().get(0); + + assertNull(expression.getColDataTypeList()); + assertEquals("name", expression.getColumnSetNotNullList().get(0).getColumnName()); + } + + @Test + public void testAlterTableVersionColumn() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE test_table DROP COLUMN version, DROP COLUMN other_column"); + assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE test_table ALTER COLUMN version TYPE VARCHAR(32) " + + "USING version::VARCHAR(32)"); } @Test @@ -690,6 +783,24 @@ public void testAlterTableRenameColumnIssue2447() throws JSQLParserException { assertEquals(expression.getColumnName(), "type2"); } + @Test + public void testMySqlAlterTableRenameVariants() throws JSQLParserException { + Alter withoutKeyword = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t1 RENAME t2"); + AlterExpression rename = withoutKeyword.getAlterExpressions().get(0); + assertEquals(AlterOperation.RENAME_TABLE, rename.getOperation()); + assertEquals("t2", rename.getNewTableName()); + assertEquals(AlterExpression.TableRenameKeyword.NONE, rename.getTableRenameKeyword()); + + Alter withAs = (Alter) assertSqlCanBeParsedAndDeparsed("ALTER TABLE t1 RENAME AS t2"); + assertEquals(AlterExpression.TableRenameKeyword.AS, + withAs.getAlterExpressions().get(0).getTableRenameKeyword()); + + Alter withTo = (Alter) assertSqlCanBeParsedAndDeparsed("ALTER TABLE t1 RENAME TO t2"); + assertEquals(AlterExpression.TableRenameKeyword.TO, + withTo.getAlterExpressions().get(0).getTableRenameKeyword()); + } + @Test public void testAlterTableForeignKeyIssue981() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed( @@ -905,13 +1016,13 @@ public void testAlterTableDefaultValueTrueIssue926() throws JSQLParserException // There shall be no COLUMN where there is no COLUMN assertStatementCanBeDeparsedAs(parsed, - "ALTER TABLE my_table ADD some_column BOOLEAN DEFAULT FALSE"); + "ALTER TABLE my_table ADD some_column BOOLEAN DEFAULT false"); } private void assertReferentialActionOnConstraint(Alter parsed, Action onUpdate, Action onDelete) { AlterExpression alterExpression = parsed.getAlterExpressions().get(0); - ForeignKeyIndex index = (ForeignKeyIndex) alterExpression.getIndex(); + ForeignKeyConstraint index = (ForeignKeyConstraint) alterExpression.getConstraint(); // remove line if deprecated methods are removed. index.setOnDeleteReferenceOption(index.getOnDeleteReferenceOption()); @@ -1109,6 +1220,25 @@ public void testIssue1875() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(stmt); } + @ParameterizedTest + @MethodSource("provideModifyColumnExistenceClauses") + public void testModifyColumnExistenceClauses(String sql, boolean usingIfExists, + boolean useIfNotExists) throws JSQLParserException { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed(sql); + AlterExpression expression = alter.getAlterExpressions().get(0); + + assertEquals(usingIfExists, expression.isUsingIfExists()); + assertEquals(useIfNotExists, expression.isUseIfNotExists()); + } + + private static Stream provideModifyColumnExistenceClauses() { + return Stream.of( + Arguments.of("ALTER TABLE t MODIFY COLUMN IF EXISTS c INT", true, false), + Arguments.of("ALTER TABLE t MODIFY IF EXISTS c INT", true, false), + Arguments.of("ALTER TABLE t MODIFY COLUMN IF NOT EXISTS c INT", false, true), + Arguments.of("ALTER TABLE t MODIFY IF NOT EXISTS c INT", false, true)); + } + @Test public void testIssue2027() throws JSQLParserException { String sql = "ALTER TABLE `foo_bar` ADD COLUMN `baz` text"; @@ -1986,13 +2116,14 @@ public void testAlterTableAddFullTextIndexWithOptions() throws JSQLParserExcepti assertEquals("col1", columnNames.get(0)); assertEquals("col2", columnNames.get(1)); - List indexSpec = index.getIndexSpec(); - assertNotNull(indexSpec); - assertEquals(4, indexSpec.size()); - assertEquals("KEY_BLOCK_SIZE = 8", indexSpec.get(0)); - assertEquals("WITH PARSER ngram", indexSpec.get(1)); - assertEquals("COMMENT 'fulltext'", indexSpec.get(2)); - assertEquals("INVISIBLE", indexSpec.get(3)); + List options = index.getSpecOptions(); + assertEquals(4, options.size()); + assertEquals("KEY_BLOCK_SIZE = 8", options.get(0).toString()); + assertEquals("WITH PARSER ngram", options.get(1).toString()); + assertEquals("COMMENT 'fulltext'", options.get(2).toString()); + assertEquals("INVISIBLE", options.get(3).toString()); + assertEquals(List.of("KEY_BLOCK_SIZE", "=", "8", "WITH", "PARSER", "ngram", "COMMENT", + "'fulltext'", "INVISIBLE"), index.getIndexSpec()); assertSqlCanBeParsedAndDeparsed(sql); } @@ -2048,13 +2179,14 @@ public void testAlterTableAddIndexWithOptions() throws JSQLParserException { assertEquals("idx_lastname", index.getName()); assertEquals("last_name", index.getColumnsNames().get(0)); - List indexSpec = index.getIndexSpec(); - assertNotNull(indexSpec); - assertEquals(4, indexSpec.size()); - assertEquals("USING BTREE", indexSpec.get(0)); - assertEquals("KEY_BLOCK_SIZE = 16", indexSpec.get(1)); - assertEquals("COMMENT 'Performance tuning'", indexSpec.get(2)); - assertEquals("VISIBLE", indexSpec.get(3)); + List options = index.getSpecOptions(); + assertEquals(4, options.size()); + assertEquals("USING BTREE", options.get(0).toString()); + assertEquals("KEY_BLOCK_SIZE = 16", options.get(1).toString()); + assertEquals("COMMENT 'Performance tuning'", options.get(2).toString()); + assertEquals("VISIBLE", options.get(3).toString()); + assertEquals(List.of("USING", "BTREE", "KEY_BLOCK_SIZE", "=", "16", "COMMENT", + "'Performance tuning'", "VISIBLE"), index.getIndexSpec()); assertSqlCanBeParsedAndDeparsed(sql); } @@ -2340,9 +2472,105 @@ public void testAlterTableAddConstraintPrimaryKeyUsingIndexName() throws JSQLPar AlterExpression alterExp = alterExpressions.get(0); assertEquals(AlterOperation.ADD, alterExp.getOperation()); - assertNotNull(alterExp.getIndex()); + assertNotNull(alterExp.getConstraint()); assertEquals(Arrays.asList("USING", "INDEX", "PK_TNWAV"), alterExp.getParameters()); assertSqlCanBeParsedAndDeparsed(sql); } + + @Test + public void testAlterTableAddConstraintPrimaryKeyUsingIndexTablespace() + throws JSQLParserException { + String sql = + "ALTER TABLE bfmcs.your_table ADD CONSTRAINT your_table_pk PRIMARY KEY (ID) USING INDEX TABLESPACE your_tablespace"; + Statement stmt = CCJSqlParserUtil.parse(sql); + assertInstanceOf(Alter.class, stmt); + + Alter alter = (Alter) stmt; + List alterExpressions = alter.getAlterExpressions(); + assertNotNull(alterExpressions); + assertEquals(1, alterExpressions.size()); + + AlterExpression alterExp = alterExpressions.get(0); + assertEquals(AlterOperation.ADD, alterExp.getOperation()); + assertEquals(Arrays.asList("USING", "INDEX", "TABLESPACE", "your_tablespace"), + alterExp.getParameters()); + + assertSqlCanBeParsedAndDeparsed(sql); + } + + @Test + public void testAlterTableAddConstraintPrimaryKeyUsingIndexNameAndTablespace() + throws JSQLParserException { + String sql = + "ALTER TABLE your_table ADD CONSTRAINT pk_test PRIMARY KEY (ID) USING INDEX idx_test TABLESPACE ts_test"; + Statement stmt = CCJSqlParserUtil.parse(sql); + assertInstanceOf(Alter.class, stmt); + + Alter alter = (Alter) stmt; + List alterExpressions = alter.getAlterExpressions(); + assertNotNull(alterExpressions); + assertEquals(1, alterExpressions.size()); + + AlterExpression alterExp = alterExpressions.get(0); + assertEquals(AlterOperation.ADD, alterExp.getOperation()); + assertEquals(Arrays.asList("USING", "INDEX", "idx_test", "TABLESPACE", "ts_test"), + alterExp.getParameters()); + + assertSqlCanBeParsedAndDeparsed(sql); + } + + @Test + public void testAlterTableAddIndexKeyPartWithPrefixLengthAndDirectionIssue2490() + throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD INDEX i05 (c1 (20) DESC)"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD INDEX i33 (c1 (20) ASC)"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD UNIQUE INDEX i34 (c1 (10) DESC)"); + } + + @Test + public void testAlterTableAddMultiValuedIndexIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t ADD INDEX i31 ((CAST(data -> '$.zips' AS UNSIGNED ARRAY)))"); + } + + @Test + public void testPostgreSqlAttachPartitionIssue2523() throws JSQLParserException { + String sql = "ALTER TABLE measurement ATTACH PARTITION measurement_y2026m01 " + + "FOR VALUES FROM ('2026-01-01') TO ('2026-02-01')"; + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + sql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + AlterExpressionPartition expression = + (AlterExpressionPartition) alter.getAlterExpressions().get(0); + + assertEquals(AlterOperation.ATTACH_PARTITION, expression.getOperation()); + assertEquals("measurement_y2026m01", expression.getPartitionTable().getName()); + assertEquals(PartitionBound.Type.RANGE, expression.getPartitionBound().getType()); + } + + @Test + public void testPostgreSqlDetachPartitionModesIssue2523() throws JSQLParserException { + String concurrently = "ALTER TABLE measurement DETACH PARTITION " + + "measurement_y2026m01 CONCURRENTLY"; + Alter concurrentAlter = (Alter) assertSqlCanBeParsedAndDeparsed( + concurrently, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + AlterExpressionPartition concurrentExpression = + (AlterExpressionPartition) concurrentAlter.getAlterExpressions().get(0); + assertEquals(AlterOperation.DETACH_PARTITION, concurrentExpression.getOperation()); + assertEquals(AlterExpressionPartition.DetachMode.CONCURRENTLY, + concurrentExpression.getDetachMode()); + + String finalizeSql = + "ALTER TABLE measurement DETACH PARTITION measurement_y2026m01 FINALIZE"; + Alter finalizeAlter = (Alter) assertSqlCanBeParsedAndDeparsed( + finalizeSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + AlterExpressionPartition finalizeExpression = + (AlterExpressionPartition) finalizeAlter.getAlterExpressions().get(0); + assertEquals(AlterExpressionPartition.DetachMode.FINALIZE, + finalizeExpression.getDetachMode()); + + assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE measurement DETACH PARTITION measurement_y2026m01", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTriggerTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTriggerTest.java new file mode 100644 index 0000000000..cf33ab0f3d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTriggerTest.java @@ -0,0 +1,109 @@ +/*- + * #%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.statement.alter; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterTriggerTest { + + @ParameterizedTest + @ValueSource(strings = {"ALTER TRIGGER trg ON users RENAME TO trg_new", + "ALTER TRIGGER \"Trg\" ON app.\"Users\" RENAME TO \"Trg 2\"", + "ALTER TRIGGER trg ON users DEPENDS ON EXTENSION ext", + "ALTER TRIGGER trg ON users NO DEPENDS ON EXTENSION ext"}) + void testRoundTrip(String sql) throws JSQLParserException { + for (Consumer consumer : List.>of(parser -> { + }, parser -> parser.withDialect(Dialect.POSTGRESQL))) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, consumer); + assertInstanceOf(AlterTrigger.class, statement); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(sql, deparsed.toString()); + assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString()); + assertEquals(2, + CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer).size()); + } + } + + @Test + void testActionsAndTable() throws JSQLParserException { + AlterTrigger rename = parse("ALTER TRIGGER trg ON app.users RENAME TO trg_new"); + assertEquals("trg", rename.getName()); + assertEquals("app.users", rename.getTable().getFullyQualifiedName()); + assertEquals(AlterTrigger.Action.RENAME, rename.getAction()); + assertEquals("trg_new", rename.getNewName()); + assertEquals(List.of("app.users"), + List.copyOf(new TablesNamesFinder().getTables((Statement) rename))); + + AlterTrigger dependency = parse("ALTER TRIGGER trg ON users NO DEPENDS ON EXTENSION ext"); + assertEquals(AlterTrigger.Action.DEPENDS_ON_EXTENSION, dependency.getAction()); + assertEquals("ext", dependency.getExtensionName()); + assertTrue(dependency.isNoDependency()); + + dependency.setNoDependency(false); + dependency.setTable(new Table("orders")); + assertEquals("ALTER TRIGGER trg ON orders DEPENDS ON EXTENSION ext", + dependency.toString()); + dependency.setAction(AlterTrigger.Action.RENAME); + dependency.setNewName("trg2"); + assertEquals("ALTER TRIGGER trg ON orders RENAME TO trg2", dependency.toString()); + + assertEquals("context:trg", rename.accept(new StatementVisitorAdapter() { + @Override + public String visit(AlterTrigger trigger, S context) { + return context + ":" + trigger.getName(); + } + }, "context")); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TRIGGER trg ENABLE", "ALTER TRIGGER trg COMPILE", + "ALTER TRIGGER trg ON t AFTER INSERT AS SELECT 1", + "ALTER TRIGGER trg ON t RENAME TO t2 CASCADE", + "ALTER TRIGGER trg ON t DEPENDS ON SCHEMA s"}) + void testOtherFormsRemainUnsupported(String sql) throws JSQLParserException { + assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql)); + assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + } + + @Test + void testValidation() { + validateNoErrors("ALTER TRIGGER trg ON users RENAME TO trg_new", 1, + DatabaseType.POSTGRESQL); + assertFalse(new Validation(List.of(DatabaseType.MYSQL), + "ALTER TRIGGER trg ON users RENAME TO trg_new").validate().isEmpty()); + } + + private static AlterTrigger parse(String sql) throws JSQLParserException { + return (AlterTrigger) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterUserTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterUserTest.java new file mode 100644 index 0000000000..8bd06f22b1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterUserTest.java @@ -0,0 +1,291 @@ +/*- + * #%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.statement.alter; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.user.PasswordLockOption; +import net.sf.jsqlparser.statement.create.user.RoleSelection; +import net.sf.jsqlparser.statement.create.user.TlsOption; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.statement.create.user.UserAuthentication; +import net.sf.jsqlparser.statement.create.user.UserAuthentication.Mode; +import net.sf.jsqlparser.statement.create.user.UserOptions; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterUserTest { + + private static AlterUser parse(String sql, Dialect dialect) throws JSQLParserException { + AlterUser alterUser = assertInstanceOf(AlterUser.class, + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(dialect))); + Statement reparsed = CCJSqlParserUtil.parse(alterUser.toString(), + parser -> parser.withDialect(dialect)); + assertEquals(sql, assertInstanceOf(AlterUser.class, reparsed).toString()); + return alterUser; + } + + private static AlterUser parse(String sql) throws JSQLParserException { + return parse(sql, Dialect.MYSQL); + } + + private static List names(List accounts) { + return accounts.stream().map(UserAccount::toString).collect(Collectors.toList()); + } + + @Test + void retainsCurrentPassword() throws JSQLParserException { + AlterUser alterUser = + parse("ALTER USER 'app'@'%' IDENTIFIED BY 'newsecret' RETAIN CURRENT PASSWORD"); + + UserAccount account = alterUser.getAccounts().get(0); + assertEquals("app", account.getUser().getValue()); + assertEquals("%", account.getHost().getValue()); + UserAuthentication authentication = account.getAuthentication(); + assertEquals(Mode.BY_PASSWORD, authentication.getMode()); + assertEquals("newsecret", authentication.getCredential().getValue()); + assertTrue(authentication.isRetainCurrentPassword()); + assertNull(authentication.getCurrentCredential()); + assertTrue(alterUser.getOptions().isEmpty()); + assertFalse(alterUser.isIfExists()); + assertFalse(alterUser.isUserFunction()); + } + + @Test + void replacesCurrentPassword() throws JSQLParserException { + UserAuthentication authentication = parse("ALTER USER 'app'@'%' IDENTIFIED WITH " + + "caching_sha2_password BY 'x' REPLACE 'old' RETAIN CURRENT PASSWORD") + .getAccounts().get(0).getAuthentication(); + + assertEquals(Mode.WITH_PLUGIN_BY_PASSWORD, authentication.getMode()); + assertEquals("caching_sha2_password", authentication.getPlugin()); + assertEquals("old", authentication.getCurrentCredential().getValue()); + assertTrue(authentication.isRetainCurrentPassword()); + } + + @Test + void namesTheCurrentUserWithParentheses() throws JSQLParserException { + AlterUser alterUser = parse("ALTER USER CURRENT_USER() IDENTIFIED BY 'x' " + + "RETAIN CURRENT PASSWORD"); + UserAccount account = alterUser.getAccounts().get(0); + + assertEquals("CURRENT_USER", account.getUser().getValue()); + assertTrue(account.isCurrentUserParentheses()); + assertFalse(alterUser.isUserFunction()); + assertFalse(parse("ALTER USER CURRENT_USER IDENTIFIED BY 'x'").getAccounts().get(0) + .isCurrentUserParentheses()); + assertEquals("CURRENT_USER()", new UserAccount() + .withUser(new StringValue("CURRENT_USER").setQuoteStr("")) + .withCurrentUserParentheses(true).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER USER 'CURRENT_USER'() IDENTIFIED BY 'x'", + "ALTER USER app() IDENTIFIED BY 'x'", + "ALTER USER CURRENT_USER()@localhost ACCOUNT LOCK"}) + void takesParenthesesOnlyAfterAnUnquotedCurrentUser(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } + + @Test + void altersTheCurrentUserWithUserFunction() throws JSQLParserException { + AlterUser alterUser = parse("ALTER USER USER() IDENTIFIED BY 'x'"); + + assertTrue(alterUser.isUserFunction()); + assertEquals("USER()", alterUser.getAccounts().get(0).getUser().getValue()); + assertEquals(Mode.BY_PASSWORD, + alterUser.getAccounts().get(0).getAuthentication().getMode()); + + AlterUser random = parse("ALTER USER IF EXISTS USER() IDENTIFIED BY RANDOM PASSWORD " + + "REPLACE 'old' RETAIN CURRENT PASSWORD"); + assertTrue(random.isIfExists()); + assertTrue(random.isUserFunction()); + + assertEquals(Mode.DISCARD_OLD_PASSWORD, + parse("ALTER USER USER() DISCARD OLD PASSWORD").getAccounts().get(0) + .getAuthentication().getMode()); + } + + @Test + void sharesOptionsWithCreateUser() throws JSQLParserException { + AlterUser alterUser = + parse("ALTER USER IF EXISTS 'app'@'%' REQUIRE NONE WITH MAX_USER_CONNECTIONS 10"); + + assertTrue(alterUser.isIfExists()); + UserOptions options = alterUser.getOptions(); + assertEquals(TlsOption.Kind.NONE, options.getTlsOptions().get(0).getKind()); + assertEquals(10, options.getResourceLimits().get(0).getValue()); + + assertEquals(PasswordLockOption.Kind.PASSWORD_EXPIRE, + parse("ALTER USER 'app'@'%' PASSWORD EXPIRE").getOptions() + .getPasswordLockOptions().get(0).getKind()); + assertEquals(PasswordLockOption.Kind.ACCOUNT_UNLOCK, + parse("ALTER USER 'app'@'%' ACCOUNT UNLOCK").getOptions() + .getPasswordLockOptions().get(0).getKind()); + } + + @Test + void altersSeveralAccounts() throws JSQLParserException { + AlterUser alterUser = parse("ALTER USER a IDENTIFIED BY 'x', 'b'@'%' DISCARD OLD PASSWORD, " + + "c ACCOUNT LOCK COMMENT 'locked'"); + + assertEquals(List.of("a IDENTIFIED BY 'x'", "'b'@'%' DISCARD OLD PASSWORD", "c"), + names(alterUser.getAccounts())); + assertEquals(Mode.DISCARD_OLD_PASSWORD, + alterUser.getAccounts().get(1).getAuthentication().getMode()); + assertEquals("locked", alterUser.getOptions().getComment().getValue()); + } + + @Test + void setsDefaultRoles() throws JSQLParserException { + RoleSelection all = parse("ALTER USER 'app'@'%' DEFAULT ROLE ALL").getOptions() + .getDefaultRoles(); + assertEquals(RoleSelection.Kind.ALL, all.getKind()); + assertTrue(all.getRoles().isEmpty()); + + assertEquals(RoleSelection.Kind.NONE, + parse("ALTER USER app DEFAULT ROLE NONE").getOptions().getDefaultRoles() + .getKind()); + + RoleSelection roles = parse("ALTER USER app DEFAULT ROLE r1, 'r2'@'%'").getOptions() + .getDefaultRoles(); + assertEquals(RoleSelection.Kind.ROLES, roles.getKind()); + assertEquals(List.of("r1", "'r2'@'%'"), names(roles.getRoles())); + } + + @Test + void parsesUnquotedAccountNames() throws JSQLParserException { + assertEquals("app", parse("ALTER USER app IDENTIFIED BY 'x'").getAccounts().get(0) + .getUser().getValue()); + UserAccount account = + parse("ALTER USER app@localhost IDENTIFIED BY 'x'").getAccounts().get(0); + assertEquals("app", account.getUser().getValue()); + assertEquals("localhost", account.getHost().getValue()); + assertTrue(parse("ALTER USER app").getOptions().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER USER \"app\"@\"%\" IDENTIFIED BY 'x' ACCOUNT LOCK", + "ALTER USER `app`@'%', app@'%', 'app'@localhost ACCOUNT LOCK", + "RENAME USER \"a\"@\"%\" TO `b`@localhost", + "SET PASSWORD FOR \"app\"@\"%\" = 'x'", + "SET DEFAULT ROLE `r`@'%' TO \"app\"@\"%\"", + "CREATE ROLE \"reader\", `writer`@'%'" + }) + void keepsQuotedAccountNamesInEveryMode(String sql) throws JSQLParserException { + // without a dialect "app" is a quoted identifier, with MySQL a double-quoted string + for (Dialect dialect : new Dialect[] {null, Dialect.MYSQL}) { + Consumer configuration = parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }; + Statement statement = + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, configuration); + assertEquals(sql, + CCJSqlParserUtil.parse(statement.toString(), configuration).toString()); + } + } + + @Test + void parsesInDefaultModeAndMariaDb() throws JSQLParserException { + String sql = "ALTER USER IF EXISTS 'app'@'%' IDENTIFIED BY 'x' REQUIRE SSL " + + "WITH MAX_QUERIES_PER_HOUR 10 PASSWORD EXPIRE NEVER ACCOUNT UNLOCK"; + parse(sql, Dialect.MARIADB); + assertInstanceOf(AlterUser.class, TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false)); + } + + @Test + void keepsPostgreSqlAlterUser() throws JSQLParserException { + for (String sql : List.of("ALTER USER app WITH LOGIN", "ALTER USER app PASSWORD 'x'", + "ALTER USER app WITH PASSWORD NULL", "ALTER USER app VALID UNTIL '2030-01-01'", + "ALTER USER app CONNECTION LIMIT 5", "ALTER USER app RENAME TO app2", + "ALTER USER app SET search_path TO app, public", "ALTER USER ALL RESET ALL", + "ALTER USER CURRENT_USER WITH PASSWORD NULL", + "ALTER USER SESSION_USER SET enable_seqscan TO on")) { + assertInstanceOf(AlterRole.class, CCJSqlParserUtil.parse(sql), sql); + assertInstanceOf(AlterRole.class, CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)), sql); + } + } + + @Test + void buildsAndDeparses() { + AlterUser alterUser = new AlterUser().withIfExists(true) + .addAccounts(new UserAccount().withUser(new StringValue("app")) + .withAuthentication(new UserAuthentication().withMode(Mode.BY_PASSWORD) + .withCredential(new StringValue("new")) + .withCurrentCredential(new StringValue("old")))) + .withOptions(new UserOptions().addPasswordLockOptions( + new PasswordLockOption(PasswordLockOption.Kind.ACCOUNT_LOCK))); + + assertDeparse(alterUser, + "ALTER USER IF EXISTS 'app' IDENTIFIED BY 'new' REPLACE 'old' ACCOUNT LOCK"); + + alterUser.withUserFunction( + new UserAuthentication().withMode(Mode.DISCARD_OLD_PASSWORD)); + assertTrue(alterUser.isUserFunction()); + assertEquals("ALTER USER IF EXISTS USER() DISCARD OLD PASSWORD ACCOUNT LOCK", + alterUser.toString()); + } + + @Test + void reportsSchemaChangeWithoutTables() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("ALTER USER 'app'@'%' ACCOUNT LOCK"); + + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(TablesNamesFinder.findTables(statement.toString())).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER USER a, b DEFAULT ROLE r", + "ALTER USER app IDENTIFIED BY 'x' DEFAULT ROLE r", + "ALTER USER app DEFAULT ROLE DEFAULT", + "ALTER USER app DEFAULT ROLE ALL EXCEPT r", + "ALTER USER app IDENTIFIED WITH auth_socket REPLACE 'x'", + "ALTER USER app IDENTIFIED WITH auth_socket AS 'h' RETAIN CURRENT PASSWORD", + "ALTER USER app IDENTIFIED BY 'x' REPLACE", + "ALTER USER USER() IDENTIFIED WITH auth_socket BY 'x'", + "ALTER USER USER() ACCOUNT LOCK", + "ALTER USER USER()", + "ALTER USER app DISCARD OLD PASSWORD RETAIN CURRENT PASSWORD", + "ALTER USER app ACCOUNT LOCK REQUIRE SSL" + }) + void rejectsSyntaxOutsideTheStatementDefinition(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/CockroachAlterPrimaryKeyTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/CockroachAlterPrimaryKeyTest.java new file mode 100644 index 0000000000..ddbeaec9e4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/CockroachAlterPrimaryKeyTest.java @@ -0,0 +1,188 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.KeyElement; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.AlterDeParser; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class CockroachAlterPrimaryKeyTest { + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.COCKROACHDB)); + } + + private static AlterExpressionPrimaryKey primaryKey(Alter alter) { + return (AlterExpressionPrimaryKey) alter.getAlterExpressions().get(0); + } + + @Test + void parsesOriginalReproducerIssue1743() throws Exception { + String sql = "ALTER TABLE FEATURE_SWITCH_CONFIG ALTER PRIMARY KEY " + + "USING COLUMNS (FEATURE_NAME) USING HASH"; + Alter alter = (Alter) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(Dialect.COCKROACHDB)); + AlterExpressionPrimaryKey action = primaryKey(alter); + assertEquals(AlterOperation.ALTER_PRIMARY_KEY, action.getOperation()); + assertEquals(ConstraintKind.PRIMARY_KEY, action.getConstraint().getKind()); + assertEquals(List.of("FEATURE_NAME"), action.getConstraint().getColumnsNames()); + assertTrue(action.isUsingHash()); + assertNull(action.getBucketCount()); + assertEquals(Set.of("FEATURE_SWITCH_CONFIG"), new TablesNamesFinder<>().getTables(alter)); + } + + @ParameterizedTest + @ValueSource(strings = {"", " USING HASH", " USING HASH WITH BUCKET_COUNT = 8", + " USING HASH WITH (bucket_count = 8)", " WITH (fillfactor = 70)"}) + void preservesKeyOrderOptionsAndRoundTrips(String suffix) throws Exception { + String sql = "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (tenant_id ASC, \"key\" DESC)" + + suffix; + Alter alter = (Alter) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(Dialect.COCKROACHDB)); + StringBuilder output = new StringBuilder(); + alter.accept(new StatementDeParser(output), null); + for (String rendered : List.of(alter.toString(), output.toString())) { + AlterExpressionPrimaryKey action = primaryKey(parse(rendered)); + assertEquals(List.of("tenant_id ASC", "\"key\" DESC"), + action.getConstraint().getColumnsNames()); + assertEquals(suffix.contains("USING HASH"), action.isUsingHash()); + if (suffix.contains("BUCKET_COUNT")) { + assertEquals(8, ((LongValue) action.getBucketCount()).getValue()); + } else { + assertNull(action.getBucketCount()); + } + if (suffix.contains("WITH (")) { + IndexOption option = action.getConstraint().getStorageParameters().get(0); + assertEquals(suffix.contains("bucket_count") ? "bucket_count" : "fillfactor", + option.getName()); + assertEquals(suffix.contains("bucket_count") ? 8 : 70, + ((LongValue) option.getValue()).getValue()); + } + } + } + + @Test + void exposesStructuredExpressionKeysAndSupportsMutation() throws Exception { + Alter alter = parse("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS " + + "(lower(name) text_ops DESC, (id + 1) ASC)"); + AlterExpressionPrimaryKey action = primaryKey(alter); + KeyElement first = action.getConstraint().getColumns().get(0); + assertTrue(first.getExpression() instanceof Function); + assertFalse(first.isExpressionParenthesized()); + assertEquals("text_ops", first.getOperatorClass()); + assertNull(first.getParams()); + first.setSortOrder(KeyElement.SortOrder.ASC); + action.setUsingHash(true); + action.setBucketCount(new LongValue(16)); + String expected = "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS " + + "(lower(name) text_ops ASC, (id + 1) ASC) USING HASH WITH BUCKET_COUNT = 16"; + TestUtils.assertDeparse(alter, expected); + assertEquals(expected, parse(expected).toString()); + } + + @Test + void requiresCockroachDialectAndKeepsExistingAlterForms() throws Exception { + String sql = "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) USING HASH"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.COCKROACHDB) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)), + dialect.name()); + } + } + for (String existing : List.of("ALTER TABLE t ADD PRIMARY KEY (id)", + "ALTER TABLE t ALTER COLUMN id TYPE BIGINT", "ALTER TABLE t DROP PRIMARY KEY")) { + String originalOutput = CCJSqlParserUtil.parse(existing).toString(); + assertEquals(existing, originalOutput.trim()); + TestUtils.assertStatementCanBeDeparsedAs(parse(existing), originalOutput); + } + } + + @Test + void visitsAndRewritesHashAndStorageExpressions() throws Exception { + Alter alter = parse("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) " + + "USING HASH WITH BUCKET_COUNT = 4 WITH (fillfactor = 70)"); + List values = new ArrayList<>(); + ExpressionVisitorAdapter visitor = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + values.add(value.getValue()); + return null; + } + }; + alter.accept(new StatementVisitorAdapter(new SelectVisitorAdapter(visitor)), + null); + assertEquals(2, values.size()); + assertEquals(Set.of(4L, 70L), Set.copyOf(values)); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 1); + } + }; + expressions.setBuilder(output); + new AlterDeParser(output, expressions).deParse(alter); + assertEquals("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) " + + "USING HASH WITH BUCKET_COUNT = 5 WITH (fillfactor = 71)", output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + } + + @Test + void keepsFollowingActionsAndStatements() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements("ALTER TABLE t ALTER PRIMARY KEY " + + "USING COLUMNS (id) USING HASH WITH BUCKET_COUNT = 4, ADD COLUMN note INT; SELECT 1;", + parser -> parser.withDialect(Dialect.COCKROACHDB)); + assertEquals(2, statements.size()); + Alter alter = (Alter) statements.get(0); + assertEquals(2, alter.getAlterExpressions().size()); + assertEquals(AlterOperation.ADD, alter.getAlterExpressions().get(1).getOperation()); + assertEquals(alter.toString(), parse(alter.toString()).toString()); + } + + @Test + void rejectsIncompleteKeysAndHashOptions() { + for (String sql : List.of("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS ()", + "ALTER TABLE t ALTER PRIMARY KEY USING (id)", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id,)", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) USING BTREE", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) USING HASH WITH BUCKET_COUNT =", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) WITH BUCKET_COUNT = 8", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) USING HASH WITH wrong = 8")) { + assertThrows(JSQLParserException.class, () -> parse(sql), sql); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java new file mode 100644 index 0000000000..535606803d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java @@ -0,0 +1,219 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetDefault; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ColumnDefaultActionTest { + @ParameterizedTest + @ValueSource(strings = {"TABLE", "VIEW"}) + void commonAccessMutatesTheExistingModel(String objectType) throws JSQLParserException { + Statement statement = parse("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 1"); + AlterExpression action = actions(statement).get(0); + ColumnDefaultAction column = action.getColumnDefaults().get(0); + assertEquals("a", column.getColumnName()); + assertEquals("1", column.getDefaultValue()); + assertSame(action instanceof RelationAlterAction ? action + : action.getColumnSetDefaultList().get(0), column); + + Expression replacement = CCJSqlParserUtil.parseExpression("(2 + 3)"); + column.setDefaultExpression(replacement); + column.setColumnName("renamed"); + assertSame(replacement, column.getDefaultExpression()); + assertEquals("(2 + 3)", column.getDefaultValue()); + assertEquals("ALTER " + objectType + " t ALTER COLUMN renamed SET DEFAULT (2 + 3)", + statement.toString()); + roundTrip(statement); + + List expressions = new ArrayList<>(); + TableDefinitionTraversal.visit(action, expressions::add, ignored -> { + }); + assertEquals(List.of(replacement), expressions); + + if (action instanceof RelationAlterAction) { + ((RelationAlterAction) action).setDefaultExpression(new LongValue(4)); + } else { + action.getColumnSetDefaultList().get(0).setDefaultExpression(new LongValue(4)); + } + assertEquals("4", column.getDefaultValue()); + roundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"TABLE", "VIEW"}) + void sharedRenderingPreservesCustomExpressionVisitors(String objectType) + throws JSQLParserException { + Statement statement = parse("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 0"); + actions(statement).get(0).getColumnDefaults().get(0) + .setDefaultExpression(CCJSqlParserUtil.parseExpression("(1 + 2)")); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT (101 + 102)", + output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + assertEquals("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT (1 + 2)", + statement.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"TABLE", "VIEW"}) + void quotedNamesAndExplicitSqlNullRoundTrip(String objectType) throws JSQLParserException { + Statement statement = + parse("ALTER " + objectType + " t ALTER COLUMN \"Old Name\" SET DEFAULT 1"); + ColumnDefaultAction column = actions(statement).get(0).getColumnDefaults().get(0); + column.setColumnName("\"New Name\""); + column.setDefaultExpression(new NullValue()); + assertEquals("NULL", column.getDefaultValue()); + assertEquals("ALTER " + objectType + " t ALTER COLUMN \"New Name\" SET DEFAULT NULL", + statement.toString()); + roundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ADD COLUMN a INT", + "ALTER TABLE t ALTER COLUMN a DROP DEFAULT", "ALTER VIEW v ALTER COLUMN a DROP DEFAULT", + "ALTER VIEW v RENAME COLUMN a TO b", + "ALTER MATERIALIZED VIEW v ALTER COLUMN a SET STATISTICS 100"}) + void unrelatedActionsDoNotAppearAsDefaults(String sql) throws JSQLParserException { + assertTrue(actions(parse(sql)).get(0).getColumnDefaults().isEmpty()); + } + + @Test + void inactiveRelationPropertiesDoNotAppearAsDefaults() { + RelationAlterAction action = new RelationAlterAction(); + action.setColumnName("a"); + action.setDefaultExpression(new LongValue(1)); + action.setColumnAction(RelationAlterAction.ColumnAction.SET_DEFAULT); + assertTrue(action.getColumnDefaults().isEmpty()); + action.setKind(RelationAlterAction.Kind.ALTER_COLUMN); + assertSame(action, action.getColumnDefaults().get(0)); + action.setColumnAction(RelationAlterAction.ColumnAction.DROP_DEFAULT); + assertTrue(action.getColumnDefaults().isEmpty()); + action.setColumnAction(RelationAlterAction.ColumnAction.SET_DEFAULT); + action.setKind(RelationAlterAction.Kind.RENAME_COLUMN); + assertTrue(action.getColumnDefaults().isEmpty()); + } + + @Test + void constraintAlterationTakesPrecedenceOverPreviouslySetDefaults() throws JSQLParserException { + Alter statement = + (Alter) CCJSqlParserUtil.parse("ALTER TABLE t ALTER COLUMN a SET DEFAULT 1"); + AlterExpression action = statement.getAlterExpressions().get(0); + ColumnDefaultAction previousDefault = action.getColumnDefaults().get(0); + action.setConstraintType("CHECK"); + action.setConstraintSymbol("c"); + action.setEnforced(true); + + assertTrue(action.getColumnDefaults().isEmpty()); + assertEquals("ALTER TABLE t ALTER CHECK c ENFORCED", statement.toString()); + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(sql.toString()).toString()); + + action.setConstraintSymbol(null); + assertSame(previousDefault, action.getColumnDefaults().get(0)); + roundTrip(statement); + } + + @Test + void opaqueLegacyConstructorIsNotParsedOrPassedToAnExpressionVisitor() { + ColumnSetDefault legacy = new ColumnSetDefault("a", "vendor_specific(?)::opaque"); + ColumnDefaultAction column = legacy; + assertNull(column.getDefaultExpression()); + assertEquals("vendor_specific(?)::opaque", column.getDefaultValue()); + StringBuilder sql = new StringBuilder(); + column.appendDefaultValueTo(sql, ignored -> { + throw new AssertionError("Opaque defaults must not enter the expression visitor"); + }); + assertEquals("vendor_specific(?)::opaque", sql.toString()); + column.setColumnName("b"); + assertEquals("b SET DEFAULT vendor_specific(?)::opaque", legacy.toString()); + column.setDefaultExpression(new LongValue(2)); + assertEquals("b SET DEFAULT 2", legacy.toString()); + column.setDefaultExpression(null); + assertNull(column.getDefaultValue()); + assertEquals("b SET DEFAULT null", legacy.toString()); + assertEquals("a SET DEFAULT null", new ColumnSetDefault("a", null).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"TABLE", "VIEW"}) + void commonLookupDoesNotExposeStructuralMutations(String objectType) + throws JSQLParserException { + Statement statement = parse("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 1"); + List defaults = actions(statement).get(0).getColumnDefaults(); + assertThrows(UnsupportedOperationException.class, defaults::clear); + defaults.get(0).setDefaultExpression(new LongValue(2)); + assertEquals("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 2", + statement.toString()); + roundTrip(statement); + } + + @Test + void lookupUsesTheExistingTableListWithoutCopyingItsEntries() { + AlterExpression action = new AlterExpression(); + action.setOperation(AlterOperation.ALTER); + ColumnSetDefault first = new ColumnSetDefault("a", "1"); + action.addColSetDefault(first); + List defaults = action.getColumnDefaults(); + ColumnSetDefault second = ColumnSetDefault.fromExpression("b", new LongValue(2)); + action.addColSetDefault(second); + assertEquals(List.of(first, second), defaults); + action.getColumnSetDefaultList().remove(first); + assertEquals(List.of(second), defaults); + action.setOperation(AlterOperation.DROP); + assertTrue(action.getColumnDefaults().isEmpty()); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static List actions(Statement statement) { + return statement instanceof Alter ? ((Alter) statement).getAlterExpressions() + : ((AlterRelation) statement).getActions(); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/ColumnIdentityActionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnIdentityActionTest.java new file mode 100644 index 0000000000..e20f111863 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnIdentityActionTest.java @@ -0,0 +1,287 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Sequence; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.IdentityDefinition; +import net.sf.jsqlparser.statement.create.table.IdentityDefinition.GenerationMode; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ColumnIdentityActionTest { + @ParameterizedTest + @ValueSource(strings = {"ADD GENERATED ALWAYS AS IDENTITY (CACHE 10)", + "SET GENERATED BY DEFAULT", "SET CACHE 10", "RESTART WITH 20", + "DROP IDENTITY IF EXISTS"}) + void exposesOriginalColumnAndKeepsEachActionKind(String clause) throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN id " + clause); + AlterExpression action = first(statement); + ColumnIdentityAction column = action.getColumnIdentityActions().get(0); + assertSame(action.getColDataTypeList().get(0), column); + assertSame(action.getColDataTypeList().get(0).getIdentityAlterations(), + column.getIdentityAlterations()); + assertNull(action.getColDataTypeList().get(0).getColDataType()); + column.setColumnName("\"New Id\""); + assertEquals("ALTER TABLE t ALTER COLUMN \"New Id\" " + clause, statement.toString()); + roundTrip(statement); + } + + @Test + void addModeAndParametersEditTheDefinitionAndFollowReplacements() throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN id " + + "ADD GENERATED BY DEFAULT AS IDENTITY (CACHE 10)"); + IdentityAlteration action = identity(statement); + IdentityDefinition original = action.getIdentityDefinition(); + assertEquals(GenerationMode.BY_DEFAULT, action.getGenerationMode()); + assertSame(original.getParameters(), action.getParameters()); + action.setGenerationMode(GenerationMode.ALWAYS); + action.getParameters().get(0).setValue(20L); + assertEquals(GenerationMode.ALWAYS, original.getGenerationMode()); + assertEquals("ALTER TABLE t ALTER COLUMN id " + + "ADD GENERATED ALWAYS AS IDENTITY (CACHE 20)", statement.toString()); + roundTrip(statement); + + IdentityDefinition replacement = new IdentityDefinition(GenerationMode.BY_DEFAULT); + action.setIdentityDefinition(replacement); + assertEquals(GenerationMode.BY_DEFAULT, action.getGenerationMode()); + assertNull(action.getParameters()); + action.setParameters(List.of(cache(30))); + assertSame(replacement.getParameters(), action.getParameters()); + replacement.setGenerationMode(GenerationMode.ALWAYS); + assertEquals(GenerationMode.ALWAYS, action.getGenerationMode()); + assertEquals("ALTER TABLE t ALTER COLUMN id " + + "ADD GENERATED ALWAYS AS IDENTITY (CACHE 30)", statement.toString()); + roundTrip(statement); + + action.setIdentityDefinition(null); + assertNull(action.getGenerationMode()); + assertNull(action.getParameters()); + action.setGenerationMode(GenerationMode.BY_DEFAULT); + assertNotSame(original, action.getIdentityDefinition()); + assertNotSame(replacement, action.getIdentityDefinition()); + assertNull(action.getParameters()); + roundTrip(statement); + } + + @Test + void addBuilderCreatesOnlyTheRequestedDefinition() { + IdentityAlteration action = new IdentityAlteration(IdentityAlteration.Kind.ADD); + assertNull(action.getGenerationMode()); + assertNull(action.getParameters()); + action.setGenerationMode(null); + action.setParameters(null); + assertNull(action.getIdentityDefinition()); + action.setParameters(List.of(cache(10))); + assertEquals(GenerationMode.BY_DEFAULT, action.getGenerationMode()); + assertEquals("ADD GENERATED BY DEFAULT AS IDENTITY (CACHE 10)", action.toString()); + IdentityDefinition definition = action.getIdentityDefinition(); + action.setGenerationMode(GenerationMode.ALWAYS); + action.setParameters(null); + assertSame(definition, action.getIdentityDefinition()); + assertEquals("ADD GENERATED ALWAYS AS IDENTITY", action.toString()); + action.setGenerationMode(null); + assertNull(definition.getGenerationMode()); + } + + @Test + void orderedSetAndRestartActionsRemainIndependentAndEditable() throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN id " + + "SET GENERATED BY DEFAULT SET CACHE 10 RESTART 20"); + ColumnIdentityAction column = first(statement).getColumnIdentityActions().get(0); + List actions = column.getIdentityAlterations(); + assertEquals(List.of(IdentityAlteration.Kind.SET_GENERATED, + IdentityAlteration.Kind.SET_PARAMETER, IdentityAlteration.Kind.RESTART), + actions.stream().map(IdentityAlteration::getKind).collect(Collectors.toList())); + actions.get(0).setGenerationMode(GenerationMode.ALWAYS); + actions.get(1).getParameters().get(0).setValue(30L); + actions.get(2).setRestartWith(null); + assertEquals("ALTER TABLE t ALTER COLUMN id SET GENERATED ALWAYS SET CACHE 30 RESTART", + statement.toString()); + roundTrip(statement); + + IdentityAlteration drop = new IdentityAlteration(IdentityAlteration.Kind.DROP); + drop.setIfExists(true); + column.setIdentityAlterations(List.of(drop)); + assertSame(drop, identity(statement)); + assertNotSame(actions, column.getIdentityAlterations()); + assertEquals("ALTER TABLE t ALTER COLUMN id DROP IDENTITY IF EXISTS", statement.toString()); + roundTrip(statement); + drop.setIfExists(false); + assertEquals("ALTER TABLE t ALTER COLUMN id DROP IDENTITY", statement.toString()); + roundTrip(statement); + } + + @Test + void unrelatedIdentityPropertiesDoNotChangeRestartOrDropRendering() { + for (IdentityAlteration.Kind kind : List.of(IdentityAlteration.Kind.RESTART, + IdentityAlteration.Kind.DROP)) { + IdentityAlteration action = new IdentityAlteration(kind); + String before = action.toString(); + action.setIdentityDefinition(new IdentityDefinition(GenerationMode.ALWAYS)); + action.setGenerationMode(GenerationMode.ALWAYS); + action.setParameters(List.of(cache(50))); + assertEquals(before, action.toString()); + } + } + + @Test + void lookupKeepsColumnMembershipSeparateFromMutableActions() throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN id RESTART WITH 20, " + + "ALTER COLUMN other TYPE bigint"); + AlterExpression action = first(statement); + List columns = action.getColumnIdentityActions(); + assertEquals(1, columns.size()); + assertTrue(statement.getAlterExpressions().get(1).getColumnIdentityActions().isEmpty()); + assertThrows(UnsupportedOperationException.class, columns::clear); + ColumnIdentityAction column = columns.get(0); + ColumnDataType stored = action.getColDataTypeList().get(0); + stored.setColDataType(new ColDataType("bigint")); + stored.setIdentityAlterations(null); + assertNull(column.getIdentityAlterations()); + assertTrue(action.getColumnIdentityActions().isEmpty()); + assertEquals(1, columns.size()); + column.setIdentityAlterations( + List.of(new IdentityAlteration(IdentityAlteration.Kind.RESTART))); + assertSame(column, action.getColumnIdentityActions().get(0)); + action.setOperation(AlterOperation.DROP); + assertTrue(action.getColumnIdentityActions().isEmpty()); + action.setOperation(AlterOperation.ALTER); + assertSame(column, action.getColumnIdentityActions().get(0)); + roundTrip(statement); + column.getIdentityAlterations().clear(); + assertTrue(action.getColumnIdentityActions().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ADD COLUMN id bigint GENERATED ALWAYS AS IDENTITY", + "ALTER TABLE t ALTER COLUMN id TYPE bigint", + "ALTER TABLE t ALTER COLUMN id SET DEFAULT 1", + "ALTER TABLE t DROP COLUMN id"}) + void excludesDeclarationsAndUnrelatedColumnChanges(String sql) throws JSQLParserException { + Alter statement = parse(sql); + assertTrue(first(statement).getColumnIdentityActions().isEmpty()); + roundTrip(statement); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("inactiveColumnBodies") + void excludesInheritedIdentityPayloadWhenAnotherBodyWins(String description, + Consumer change) throws JSQLParserException { + AlterExpression action = first(parse("ALTER TABLE t ALTER COLUMN id RESTART")); + change.accept(action); + assertTrue(action.getColumnIdentityActions().isEmpty(), description); + } + + static Stream inactiveColumnBodies() { + return Stream.of( + inactive("different operation", action -> action.setOperation(AlterOperation.DROP)), + inactive("named column", action -> action.setColumnName("other")), + inactive("comment", action -> action.setCommentText("'text'")), + inactive("constraint header", action -> { + action.setConstraintType("CONSTRAINT"); + action.setConstraintSymbol("c"); + }), + inactive("index rename", action -> action.setOldIndex(new Index().withName("old"))), + inactive("drop default", action -> action.addColDropDefault( + new AlterExpression.ColumnDropDefault("other"))), + inactive("set default", action -> action.addColSetDefault( + new AlterExpression.ColumnSetDefault("other", "1"))), + inactive("visibility", action -> action.addColSetVisibility( + new AlterExpression.ColumnSetVisibility("other", true)))); + } + + private static Arguments inactive(String description, Consumer change) { + return Arguments.of(description, change); + } + + @Test + void changedDropOperationUsesItsGeneralRendererPrecedence() { + AlterExpressionDrop action = new AlterExpressionDrop(); + action.setOperation(AlterOperation.ALTER); + ColumnDataType column = new ColumnDataType(false).withColumnName("id"); + column.setIdentityAlterations( + List.of(new IdentityAlteration(IdentityAlteration.Kind.RESTART))); + action.addColDataType(column); + assertEquals(List.of(column), action.getColumnIdentityActions()); + action.addColSetDefault(new AlterExpression.ColumnSetDefault("other", "1")); + action.setConstraintType("CONSTRAINT"); + action.setConstraintSymbol("c"); + action.setOldIndex(new Index().withName("old")); + assertEquals("ALTER id RESTART", action.toString()); + assertEquals(List.of(column), action.getColumnIdentityActions()); + column.setColumnName("renamed"); + assertEquals("ALTER renamed RESTART", action.toString()); + action.setColumnName("other"); + assertTrue(action.getColumnIdentityActions().isEmpty()); + assertEquals("ALTER other", action.toString()); + action.setColumnName(null); + action.setOperation(AlterOperation.DROP); + assertTrue(action.getColumnIdentityActions().isEmpty()); + } + + @ParameterizedTest + @MethodSource("fixedBodies") + void fixedActionRenderersDoNotExposeInactiveColumnIdentityData(AlterExpression action) { + action.setOperation(AlterOperation.ALTER); + ColumnDataType column = new ColumnDataType(false).withColumnName("id"); + column.setIdentityAlterations( + List.of(new IdentityAlteration(IdentityAlteration.Kind.RESTART))); + action.addColDataType(column); + assertTrue(action.getColumnIdentityActions().isEmpty()); + } + + static Stream fixedBodies() { + return Stream.of(new AlterConstraintAttributes(), new AlterExpressionCharset(), + new AlterExpressionOrderBy(), new AlterExpressionPartition(), + new AlterExpressionPrimaryKey(), new AlterExpressionRename(), + new AlterExpressionTableOption(), new AlterForeignDataOptions(), + new RelationAlterAction()); + } + + private static Sequence.Parameter cache(long value) { + return new Sequence.Parameter(Sequence.ParameterType.CACHE).withValue(value); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static AlterExpression first(Alter statement) { + return statement.getAlterExpressions().get(0); + } + + private static IdentityAlteration identity(Alter statement) { + return first(statement).getColumnIdentityActions().get(0).getIdentityAlterations().get(0); + } + + private static void roundTrip(Alter statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/ColumnRenameActionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnRenameActionTest.java new file mode 100644 index 0000000000..d3a91e4010 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnRenameActionTest.java @@ -0,0 +1,165 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ColumnRenameActionTest { + @ParameterizedTest + @CsvSource({"MYSQL, ALTER TABLE t RENAME COLUMN old_col TO new_col", + "POSTGRESQL, ALTER TABLE t RENAME COLUMN old_col TO new_col", + "POSTGRESQL, ALTER TABLE t RENAME old_col TO new_col", + "POSTGRESQL, ALTER VIEW v RENAME COLUMN old_col TO new_col", + "POSTGRESQL, ALTER VIEW v RENAME old_col TO new_col", + "POSTGRESQL, ALTER MATERIALIZED VIEW v RENAME COLUMN old_col TO new_col"}) + void commonAndLegacyAccessorsEditTheSameNames(Dialect dialect, String sql) + throws JSQLParserException { + Statement statement = parse(sql, dialect); + AlterExpression action = action(statement); + ColumnRenameAction rename = action.getColumnRename(); + assertNotNull(rename); + assertEquals("old_col", rename.getSourceName()); + assertEquals("new_col", rename.getTargetName()); + + String quote = dialect == Dialect.MYSQL ? "`" : "\""; + String sourceName = quote + "old column" + quote; + String targetName = quote + "new column" + quote; + rename.setSourceName(sourceName); + rename.setTargetName(targetName); + if (action instanceof RelationAlterAction) { + RelationAlterAction relation = (RelationAlterAction) action; + assertEquals(sourceName, relation.getColumnName()); + assertEquals(targetName, relation.getNewName()); + } else { + assertEquals(sourceName, action.getColumnOldName()); + assertEquals(targetName, action.getColumnName()); + } + assertRoundTrip(sql.replace("old_col", sourceName) + .replace("new_col", targetName), statement, dialect); + + if (action instanceof RelationAlterAction) { + RelationAlterAction relation = (RelationAlterAction) action; + relation.setColumnName("legacy_source"); + relation.setNewName("legacy_target"); + } else { + action.setColumnOldName("legacy_source"); + action.setColumnName("legacy_target"); + } + assertEquals("legacy_source", rename.getSourceName()); + assertEquals("legacy_target", rename.getTargetName()); + assertSame(rename, action.getColumnRename()); + assertRoundTrip(sql.replace("old_col", "legacy_source") + .replace("new_col", "legacy_target"), statement, dialect); + } + + @ParameterizedTest + @CsvSource({"POSTGRESQL, ALTER TABLE t RENAME TO other_table", + "MYSQL, ALTER TABLE t RENAME INDEX old_index TO new_index", + "MYSQL, ALTER TABLE t RENAME KEY old_key TO new_key", + "POSTGRESQL, ALTER TABLE t RENAME CONSTRAINT old_constraint TO new_constraint", + "POSTGRESQL, ALTER VIEW v RENAME TO other_view", + "POSTGRESQL, ALTER INDEX ix RENAME TO other_index", + "POSTGRESQL, ALTER TABLE t ADD COLUMN c INT", + "POSTGRESQL, ALTER VIEW v SET SCHEMA other_schema"}) + void otherOperationsDoNotExposeAColumnRename(Dialect dialect, String sql) + throws JSQLParserException { + Statement statement = parse(sql, dialect); + assertNull(action(statement).getColumnRename()); + assertRoundTrip(statement.toString(), statement, dialect); + } + + @Test + void switchingTableOperationsKeepsInactiveNamesWithoutExposingThem() + throws JSQLParserException { + Statement statement = parse("ALTER TABLE t RENAME COLUMN old_col TO new_col"); + AlterExpression action = action(statement); + ColumnRenameAction rename = action.getColumnRename(); + action.setOperation(AlterOperation.RENAME_TABLE); + action.setNewTableName("other_table"); + assertNull(action.getColumnRename()); + assertEquals("old_col", action.getColumnOldName()); + assertEquals("new_col", action.getColumnName()); + assertRoundTrip("ALTER TABLE t RENAME TO other_table", statement); + + action.setOperation(AlterOperation.RENAME); + assertSame(rename, action.getColumnRename()); + assertRoundTrip("ALTER TABLE t RENAME COLUMN old_col TO new_col", statement); + } + + @Test + void switchingRelationKindsKeepsInactiveNamesWithoutExposingThem() + throws JSQLParserException { + Statement statement = parse("ALTER VIEW v RENAME COLUMN old_col TO new_col"); + RelationAlterAction action = (RelationAlterAction) action(statement); + ColumnRenameAction rename = action.getColumnRename(); + action.setKind(RelationAlterAction.Kind.SET_SCHEMA); + action.setValue("other_schema"); + assertNull(action.getColumnRename()); + assertEquals("old_col", action.getColumnName()); + assertEquals("new_col", action.getNewName()); + assertRoundTrip("ALTER VIEW v SET SCHEMA other_schema", statement); + + action.setKind(RelationAlterAction.Kind.RENAME_COLUMN); + assertSame(rename, action.getColumnRename()); + assertRoundTrip("ALTER VIEW v RENAME COLUMN old_col TO new_col", statement); + } + + private static Statement parse(String sql) throws JSQLParserException { + return parse(sql, Dialect.POSTGRESQL); + } + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static AlterExpression action(Statement statement) { + return statement instanceof Alter + ? ((Alter) statement).getAlterExpressions().get(0) + : ((AlterRelation) statement).getActions().get(0); + } + + private static void assertRoundTrip(String expected, Statement statement) + throws JSQLParserException { + assertRoundTrip(expected, statement, Dialect.POSTGRESQL); + } + + private static void assertRoundTrip(String expected, Statement statement, Dialect dialect) + throws JSQLParserException { + assertEquals(expected, statement.toString()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(expected, output.toString()); + Statement reparsed = parse(output.toString(), dialect); + assertEquals(statement.getClass(), reparsed.getClass()); + assertEquals(action(statement).getClass(), action(reparsed).getClass()); + assertEquals(expected, reparsed.toString()); + ColumnRenameAction rename = action(statement).getColumnRename(); + ColumnRenameAction reparsedRename = action(reparsed).getColumnRename(); + if (rename == null) { + assertNull(reparsedRename); + } else { + assertNotNull(reparsedRename); + assertEquals(rename.getSourceName(), reparsedRename.getSourceName()); + assertEquals(rename.getTargetName(), reparsedRename.getTargetName()); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/DeparseWhitespaceTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/DeparseWhitespaceTest.java new file mode 100644 index 0000000000..6dd6bee377 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/DeparseWhitespaceTest.java @@ -0,0 +1,141 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.api.Test; + +/** Renderers separate words with exactly one space and add no trailing space. */ +class DeparseWhitespaceTest { + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t DROP PRIMARY KEY", + "ALTER TABLE t DROP PRIMARY KEY CASCADE", + "ALTER TABLE t DROP PRIMARY KEY, DROP COLUMN c", + "RENAME TABLE t TO t2", + "RENAME TABLE a TO b, c TO d", + "RENAME TABLE IF EXISTS a TO b", + "RENAME a TO b", + "ALTER TABLE t ADD INDEX i (body (10))", + "ALTER TABLE t ADD KEY k (a, b)", + "CREATE TABLE t (a INT, INDEX i (a))", + "ALTER TABLE t ALTER COLUMN c SET INVISIBLE", + "ALTER TABLE t ALTER COLUMN c SET VISIBLE", + "ALTER TABLE t ALTER c SET INVISIBLE", + "ALTER TABLE t ALTER INDEX i INVISIBLE", + "ALTER TABLE t ALTER INDEX i VISIBLE", + "ALTER TABLE t ALTER COLUMN c SET VISIBLE, ALTER INDEX i VISIBLE", + "ALTER TABLE t FORCE", + "ALTER TABLE t FORCE, ALGORITHM = INPLACE", + "CREATE TABLE t (a JSON DEFAULT (JSON_ARRAY()))", + "ALTER TABLE t ADD COLUMN j JSON DEFAULT (JSON_OBJECT())"}) + void rendersSingleSpaces(String sql) throws JSQLParserException { + assertRendersExactly(sql, Dialect.MYSQL); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ENABLE ROW LEVEL SECURITY", + "ALTER TABLE t DISABLE ROW LEVEL SECURITY", + "ALTER TABLE t FORCE ROW LEVEL SECURITY", + "ALTER TABLE t NO FORCE ROW LEVEL SECURITY", + "ALTER TABLE t ENABLE ROW LEVEL SECURITY, FORCE ROW LEVEL SECURITY", + "CREATE INDEX ON t (a)"}) + void rendersSingleSpacesForPostgreSql(String sql) throws JSQLParserException { + assertRendersExactly(sql, Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t FORCE", + "ALTER TABLE t MODIFY (c INT)", + "ALTER TABLE t MODIFY COLUMN (c INT NOT NULL)", + "ALTER TABLE t ADD (c INT), ADD d INT", + "ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uk (a)", + "SELECT JSON_ARRAY() FROM t", + "SELECT JSON_OBJECT() FROM t"}) + void rendersSingleSpacesWithoutDialect(String sql) throws JSQLParserException { + assertRendersExactly(sql, null); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT JSON_ARRAY(1, 2) AS a FROM t", + "SELECT JSON_ARRAY(NULL ON NULL) AS a FROM t", + "SELECT JSON_OBJECT(KEY 'a' VALUE 1) AS o FROM t", + "SELECT JSON_OBJECT(ABSENT ON NULL) AS o FROM t"}) + void jsonConstructorsAddNoDoubleOrTrailingSpace(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + String rendered = statement.toString(); + assertFalse(rendered.contains(" "), rendered); + assertEquals(rendered, deparse(statement)); + } + + @Test + void columnVisibilityHasSingleSpaces() { + assertEquals("c SET INVISIBLE", + new AlterExpression.ColumnSetVisibility("c", false).toString()); + assertEquals("c SET VISIBLE", + new AlterExpression.ColumnSetVisibility("c", true).toString()); + } + + @Test + void exchangePartitionHasNoTrailingSpace() { + AlterExpression expression = new AlterExpression() + .withOperation(AlterOperation.EXCHANGE_PARTITION); + expression.setPartitions(List.of("p0")); + expression.setExchangePartitionTableName("t2"); + expression.setExchangePartitionWithValidation(true); + assertEquals("EXCHANGE PARTITION p0 WITH TABLE t2 WITH VALIDATION", + expression.toString()); + } + + @Test + void indexWithoutDeclarationHasNoLeadingSpace() throws JSQLParserException { + CreateIndex createIndex = (CreateIndex) CCJSqlParserUtil.parse("CREATE INDEX ON t (a)", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals("(a)", createIndex.getIndex().toString()); + assertEquals("(a)", new Index().withColumnsNames(List.of("a")).toString()); + assertEquals("INDEX i INVISIBLE", new Index().withIndexKeyword("INDEX").withName("i") + .withIndexSpec(List.of("INVISIBLE")).toString()); + } + + private static void assertRendersExactly(String sql, Dialect dialect) + throws JSQLParserException { + Statement statement = dialect == null ? CCJSqlParserUtil.parse(sql) + : CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + assertEquals(sql, statement.toString()); + assertEquals(sql, deparse(statement)); + } + + private static String deparse(Statement statement) { + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + return deparsed.toString(); + } + + @Test + void indexDeclarationHasNoLeadingSpace() { + assertEquals("i2", new Index().withName("i2").toString()); + assertEquals("KEY k (a)", new Index().withIndexKeyword("KEY").withName("k") + .withColumnsNames(java.util.List.of("a")).toString()); + assertEquals("UNIQUE INDEX u (a)", new Index().withType("UNIQUE").withIndexKeyword("INDEX") + .withName("u").withColumnsNames(java.util.List.of("a")).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/IdentityRestartValueTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/IdentityRestartValueTest.java new file mode 100644 index 0000000000..c73b2fc103 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/IdentityRestartValueTest.java @@ -0,0 +1,84 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class IdentityRestartValueTest { + @ParameterizedTest + @ValueSource(strings = {"20", "WITH 20", "+20", "-20", "WITH -20", "9223372036854775807"}) + void representsOptionalWithUsingTheSameValue(String value) throws JSQLParserException { + for (Dialect dialect : new Dialect[] {null, Dialect.POSTGRESQL}) { + Alter alter = parse("ALTER TABLE t ALTER COLUMN id RESTART " + value, dialect); + IdentityAlteration restart = identity(alter).get(0); + assertEquals(IdentityAlteration.Kind.RESTART, restart.getKind()); + assertEquals(Long.valueOf(value.replace("WITH ", "")), restart.getRestartWith()); + roundTrip(alter, dialect); + } + } + + @Test + void editsAndConstructsRestartAndPreservesBareRestart() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ALTER COLUMN id RESTART", Dialect.POSTGRESQL); + assertNull(identity(alter).get(0).getRestartWith()); + identity(alter).get(0).setRestartWith(20L); + assertEquals("ALTER TABLE t ALTER COLUMN id RESTART WITH 20", alter.toString()); + roundTrip(alter, Dialect.POSTGRESQL); + alter.getAlterExpressions().get(0).getColDataTypeList().get(0).setIdentityAlterations( + List.of(new IdentityAlteration(IdentityAlteration.Kind.RESTART) + .withRestartWith(40L))); + assertEquals("ALTER TABLE t ALTER COLUMN id RESTART WITH 40", alter.toString()); + roundTrip(alter, Dialect.POSTGRESQL); + identity(alter).get(0).setRestartWith(null); + assertEquals("ALTER TABLE t ALTER COLUMN id RESTART", alter.toString()); + } + + @Test + void respectsIdentityAndAlterActionBoundaries() throws JSQLParserException { + Alter alter = parse( + "ALTER TABLE t ALTER COLUMN id SET CACHE 10 RESTART 20 SET NO CYCLE, ADD COLUMN extra INT", + Dialect.POSTGRESQL); + assertEquals(3, identity(alter).size()); + assertEquals(2, alter.getAlterExpressions().size()); + roundTrip(alter, Dialect.POSTGRESQL); + roundTrip(parse("ALTER TABLE t ALTER COLUMN id RESTART SET CACHE 10", Dialect.POSTGRESQL), + Dialect.POSTGRESQL); + assertThrows(JSQLParserException.class, + () -> parse("ALTER TABLE t ALTER COLUMN id RESTART WITH", Dialect.POSTGRESQL)); + } + + private static List identity(Alter alter) { + return alter.getAlterExpressions().get(0).getColDataTypeList().get(0) + .getIdentityAlterations(); + } + + private static Alter parse(String sql, Dialect dialect) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, p -> { + if (dialect != null) { + p.withDialect(dialect); + } + }); + } + + private static void roundTrip(Alter alter, Dialect dialect) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + alter.accept(new StatementDeParser(sql), null); + assertEquals(alter.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/IndexOptionBoundaryTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/IndexOptionBoundaryTest.java new file mode 100644 index 0000000000..f53b06491f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/IndexOptionBoundaryTest.java @@ -0,0 +1,96 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.Index; +import java.util.List; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class IndexOptionBoundaryTest { + @ParameterizedTest + @ValueSource(strings = {"UNIQUE", "PRIMARY KEY"}) + void tablespaceBelongsToConstraintOptions(String key) throws JSQLParserException { + Alter alter = (Alter) parse("ALTER TABLE t ADD CONSTRAINT k " + key + + " (id) USING INDEX TABLESPACE pg_default DEFERRABLE", Dialect.POSTGRESQL); + KeyConstraint index = (KeyConstraint) alter.getAlterExpressions().get(0).getConstraint(); + assertEquals("pg_default", index.getTableSpace()); + assertNull(index.getUsing()); + assertTrue(index.getConstraintAttributes().getDeferrable()); + index.setTableSpace("other_space"); + assertEquals("ALTER TABLE t ADD CONSTRAINT k " + key + + " (id) USING INDEX TABLESPACE other_space DEFERRABLE", alter.toString()); + assertRoundTrip(alter, Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = {"SECONDARY_ENGINE_ATTRIBUTE = '{}'", + "SECONDARY_ENGINE_ATTRIBUTE '{\"key\": \"value\"}'", + "SECONDARY_ENGINE_ATTRIBUTE = '{}' VISIBLE COMMENT 'index'"}) + void mysqlAddIndexAttributes(String options) throws JSQLParserException { + Alter alter = (Alter) parse("ALTER TABLE t ADD INDEX ix (id) " + options, Dialect.MYSQL); + assertEquals("ix", alter.getAlterExpressions().get(0).getIndex().getName()); + assertEquals("ALTER TABLE t ADD INDEX ix (id) " + options, alter.toString()); + assertRoundTrip(alter, Dialect.MYSQL); + } + + @Test + void permissiveIndexSuffixRemainsAfterItsStructuredOptions() throws JSQLParserException { + String sql = "CREATE TABLE t (id INT, INDEX ix (id) INCLUDE (id) " + + "WITH (fillfactor = 70) USING INDEX TABLESPACE fast DEFERRABLE)"; + CreateTable statement = (CreateTable) CCJSqlParserUtil.parse(sql); + Index index = statement.getIndexes().get(0); + assertEquals(List.of("id"), index.getIncludeColumns()); + assertEquals(List.of("DEFERRABLE"), index.getTailParameters()); + assertEquals(sql, statement.toString()); + index.setTableSpace("other_space"); + assertEquals(sql.replace("TABLESPACE fast", "TABLESPACE other_space"), + statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(statement.toString(), deparsed.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(deparsed.toString()).toString()); + } + + @Test + void sharedTableAttributeRemainsMutable() throws JSQLParserException { + Alter alter = + (Alter) parse("ALTER TABLE t SECONDARY_ENGINE_ATTRIBUTE = '{}'", Dialect.MYSQL); + AlterExpressionTableOption action = + (AlterExpressionTableOption) alter.getAlterExpressions().get(0); + TableOption option = action.getStructuredTableOption(); + assertEquals(TableOption.Kind.SECONDARY_ENGINE_ATTRIBUTE, option.getKind()); + option.setValue("'{\"key\":1}'"); + assertEquals("ALTER TABLE t SECONDARY_ENGINE_ATTRIBUTE = '{\"key\":1}'", alter.toString()); + assertRoundTrip(alter, Dialect.MYSQL); + } + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + } + + private static void assertRoundTrip(Statement statement, Dialect dialect) + throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/InformixConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/InformixConstraintTest.java new file mode 100644 index 0000000000..64bf857795 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/InformixConstraintTest.java @@ -0,0 +1,191 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.util.deparser.TableElementDeParser; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.test.TestUtils; +import java.util.Set; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint.ConstraintNamePosition; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class InformixConstraintTest { + static Stream definitions() { + return Stream.of("PRIMARY KEY (column)", "UNIQUE (column)", + "FOREIGN KEY (column) REFERENCES referenced_table(referenced_column)", + "CHECK (id > 0)", "PRIMARY KEY (id, tenant_id)", + "FOREIGN KEY (id, tenant_id) REFERENCES parent(id, tenant_id) ON DELETE CASCADE"); + } + + @ParameterizedTest + @MethodSource("definitions") + void preservesNamePlacement(String definition) throws Exception { + for (String suffix : List.of("", " CONSTRAINT constraint_name", + " CONSTRAINT \"constraint name\"")) { + String sql = "ALTER TABLE table_name ADD CONSTRAINT " + definition + suffix; + Alter statement = (Alter) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.INFORMIX)); + NamedConstraint constraint = + (NamedConstraint) statement.getAlterExpressions().get(0).getConstraint(); + assertEquals(ConstraintNamePosition.AFTER, constraint.getConstraintNamePosition()); + assertTrue(constraint.isUseConstraintKeyword()); + assertEquals(suffix.isEmpty() ? null : suffix.substring(" CONSTRAINT ".length()), + constraint.getName()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed), null); + assertEquals(statement.toString(), deparsed.toString()); + assertEquals(statement.toString(), + CCJSqlParserUtil.parse(deparsed.toString(), + parser -> parser.withDialect(Dialect.INFORMIX)).toString()); + assertTrue(statement.toString().contains("ADD CONSTRAINT " + definition)); + if (!suffix.isEmpty()) { + assertTrue(statement.toString().endsWith(suffix)); + } + } + } + + @ParameterizedTest + @MethodSource("definitions") + void requiresInformixDialectForTrailingNames(String definition) { + for (String suffix : List.of(" CONSTRAINT constraint_name", + " CONSTRAINT \"constraint name\"")) { + String sql = "ALTER TABLE table_name ADD CONSTRAINT " + definition + suffix; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.INFORMIX) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(dialect)), + dialect.name()); + } + } + } + } + + @Test + void retainsDefaultInterpretationOfPrimaryAsConstraintName() throws Exception { + String sql = "ALTER TABLE t ADD CONSTRAINT PRIMARY KEY (id)"; + Alter statement = (Alter) CCJSqlParserUtil.parse(sql); + NamedConstraint constraint = + (NamedConstraint) statement.getAlterExpressions().get(0).getConstraint(); + assertEquals("PRIMARY", constraint.getName()); + assertEquals("KEY", constraint.getType()); + assertEquals(ConstraintNamePosition.BEFORE, constraint.getConstraintNamePosition()); + assertEquals(sql, statement.toString()); + } + + @Test + void exposesForeignKeyAndMutableName() throws Exception { + Alter statement = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE child ADD CONSTRAINT FOREIGN KEY (id) REFERENCES parent(id) CONSTRAINT fk_child", + parser -> parser.withDialect(Dialect.INFORMIX)); + ForeignKeyConstraint key = + (ForeignKeyConstraint) statement.getAlterExpressions().get(0).getConstraint(); + assertEquals(List.of("id"), key.getColumnsNames()); + assertEquals("parent", key.getTable().getName()); + assertEquals(List.of("id"), key.getReferencedColumnNames()); + key.setName("renamed_fk"); + assertTrue(statement.toString().endsWith("REFERENCES parent(id) CONSTRAINT renamed_fk")); + key.setName((String) null); + assertFalse(statement.toString().contains("fk_child")); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(statement.toString(), + parser -> parser.withDialect(Dialect.INFORMIX)).toString()); + } + + @Test + void keepsFollowingAlterActionsAndStatements() throws Exception { + String sql = + "ALTER TABLE t ADD CONSTRAINT PRIMARY KEY (id) CONSTRAINT pk_t, ADD COLUMN note INT; SELECT 1;"; + Statements statements = CCJSqlParserUtil.parseStatements(sql, + parser -> parser.withDialect(Dialect.INFORMIX)); + assertEquals(2, statements.size()); + assertEquals(2, ((Alter) statements.get(0)).getAlterExpressions().size()); + } + + @Test + void retainsLeadingNamesAndBuilders() throws Exception { + NamedConstraint built = new KeyConstraint().withType("PRIMARY KEY").withName("pk_t") + .withColumnsNames(List.of("id")); + assertEquals(ConstraintNamePosition.BEFORE, built.getConstraintNamePosition()); + assertEquals("CONSTRAINT pk_t PRIMARY KEY (id)", built.toString()); + for (String definition : List.of("PRIMARY KEY (id)", "UNIQUE (id)", + "FOREIGN KEY (id) REFERENCES parent(id)", "CHECK (id > 0)")) { + String sql = "ALTER TABLE t ADD CONSTRAINT c " + definition; + for (Alter statement : List.of((Alter) CCJSqlParserUtil.parse(sql), + (Alter) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.INFORMIX)))) { + assertEquals(ConstraintNamePosition.BEFORE, + ((NamedConstraint) statement.getAlterExpressions().get(0).getConstraint()) + .getConstraintNamePosition()); + assertTrue(statement.toString().contains("CONSTRAINT c " + definition)); + } + } + } + + @Test + void keepsCheckExpressionVisitorAndForeignTableTraversal() throws Exception { + Alter statement = (Alter) CCJSqlParserUtil + .parse("ALTER TABLE child ADD CONSTRAINT CHECK (id > 0) CONSTRAINT positive_id", + parser -> parser.withDialect(Dialect.INFORMIX)); + CheckConstraint check = + (CheckConstraint) statement + .getAlterExpressions().get(0).getConstraint(); + StringBuilder builder = new StringBuilder(); + ExpressionDeParser visitor = + new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, + S context) { + getBuilder().append("renamed_id"); + return getBuilder(); + } + }; + visitor.setBuilder(builder); + new TableElementDeParser(builder, visitor).deParse(check); + assertEquals("CONSTRAINT CHECK (renamed_id > 0) CONSTRAINT positive_id", + builder.toString()); + assertEquals(Set.of("child", "parent"), + new TablesNamesFinder<>().getTables(CCJSqlParserUtil.parse( + "ALTER TABLE child ADD CONSTRAINT FOREIGN KEY (id) REFERENCES parent(id) CONSTRAINT fk", + parser -> parser.withDialect(Dialect.INFORMIX)))); + } + + @Test + void rejectsMissingOrDuplicateNames() { + for (String sql : List.of( + "ALTER TABLE t ADD CONSTRAINT PRIMARY KEY (id) CONSTRAINT", + "ALTER TABLE t ADD CONSTRAINT UNIQUE (id) CONSTRAINT a CONSTRAINT b", + "ALTER TABLE t ADD CONSTRAINT FOREIGN KEY (id) CONSTRAINT fk", + "ALTER TABLE t MODIFY CONSTRAINT UNIQUE (id) CONSTRAINT uk")) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.INFORMIX))); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java new file mode 100644 index 0000000000..1648373a31 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java @@ -0,0 +1,366 @@ +/*- + * #%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.statement.alter; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.TableMaintenanceStatement.Modifier; +import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** MySQL 8.4 {@code ALTER TABLE} partition syntax verified against the MySQL manual. */ +public class MySQLAlterTablePartitionTest { + + private static final List MYSQL84_PARTITION_OPERATIONS = List.of( + "ALTER TABLE sales ADD PARTITION " + + "(PARTITION p2027 VALUES LESS THAN (2028) ENGINE = InnoDB)", + "ALTER TABLE sales DROP PARTITION p2023, `p2024`", + "ALTER TABLE sales DISCARD PARTITION p2024 TABLESPACE", + "ALTER TABLE sales IMPORT PARTITION ALL TABLESPACE", + "ALTER TABLE sales TRUNCATE PARTITION ALL", + "ALTER TABLE sales COALESCE PARTITION 2", + "ALTER TABLE sales REORGANIZE PARTITION p0, p1 INTO " + + "(PARTITION p_low VALUES LESS THAN (100), " + + "PARTITION p_high VALUES LESS THAN MAXVALUE)", + "ALTER TABLE sales EXCHANGE PARTITION p0 WITH TABLE archive.sales_2024", + "ALTER TABLE sales EXCHANGE PARTITION p0 WITH TABLE archive.sales_2024 " + + "WITH VALIDATION", + "ALTER TABLE sales EXCHANGE PARTITION p0 WITH TABLE archive.sales_2024 " + + "WITHOUT VALIDATION", + "ALTER TABLE sales ANALYZE PARTITION p0, p1", + "ALTER TABLE sales CHECK PARTITION ALL", + "ALTER TABLE sales OPTIMIZE PARTITION p0", + "ALTER TABLE sales REBUILD PARTITION p0", + "ALTER TABLE sales REPAIR PARTITION ALL", + "ALTER TABLE sales REMOVE PARTITIONING", + "ALTER TABLE clients ADD PARTITION PARTITIONS 6", + "ALTER TABLE clients ADD PARTITION NO_WRITE_TO_BINLOG PARTITIONS 6", + "ALTER TABLE clients ADD PARTITION LOCAL PARTITIONS 6", + "ALTER TABLE sales ADD PARTITION NO_WRITE_TO_BINLOG " + + "(PARTITION p2027 VALUES LESS THAN (2028))", + "ALTER TABLE sales ADD PARTITION LOCAL (PARTITION p2027 VALUES LESS THAN (2028))", + "ALTER TABLE clients COALESCE PARTITION NO_WRITE_TO_BINLOG 2", + "ALTER TABLE clients COALESCE PARTITION LOCAL 2", + "ALTER TABLE sales REORGANIZE PARTITION NO_WRITE_TO_BINLOG p0 INTO " + + "(PARTITION p0a VALUES LESS THAN (50), " + + "PARTITION p0b VALUES LESS THAN (100))", + "ALTER TABLE sales ANALYZE PARTITION NO_WRITE_TO_BINLOG p0, p1", + "ALTER TABLE sales OPTIMIZE PARTITION LOCAL ALL", + "ALTER TABLE sales REBUILD PARTITION NO_WRITE_TO_BINLOG ALL", + "ALTER TABLE sales REPAIR PARTITION LOCAL p0"); + + private static final List MYSQL84_PARTITIONING_CLAUSES = List.of( + "ALTER TABLE sales PARTITION BY HASH (id) PARTITIONS 8", + "ALTER TABLE sales PARTITION BY LINEAR KEY ALGORITHM = 1 (id) PARTITIONS 4", + "ALTER TABLE sales PARTITION BY RANGE (YEAR(created_at)) " + + "SUBPARTITION BY HASH (id) SUBPARTITIONS 2 " + + "(PARTITION p2026 VALUES LESS THAN (2027), " + + "PARTITION pmax VALUES LESS THAN MAXVALUE)", + "ALTER TABLE sales PARTITION BY LIST COLUMNS (region, tier) " + + "(PARTITION p_eu VALUES IN (('eu', 1), ('eu', 2)), " + + "PARTITION p_us VALUES IN (('us', 1)))"); + + private static Stream mysql84PartitionOperations() { + return MYSQL84_PARTITION_OPERATIONS.stream(); + } + + private static Stream mysql84PartitioningClauses() { + return MYSQL84_PARTITIONING_CLAUSES.stream(); + } + + @ParameterizedTest + @MethodSource("mysql84PartitionOperations") + public void testPartitionOperationsUseStructuredSubtype(String sql) + throws JSQLParserException { + assertInstanceOf(AlterExpressionPartition.class, parseExpression(sql)); + assertSqlCanBeParsedAndDeparsed(sql); + } + + @ParameterizedTest + @MethodSource("mysql84PartitioningClauses") + public void testPartitionByUsesCreateTablePartitioningModel(String sql) + throws JSQLParserException { + AlterExpressionPartition expression = parseExpression(sql); + assertEquals(AlterOperation.PARTITION_BY, expression.getOperation()); + assertNotNull(expression.getPartitioning()); + assertSqlCanBeParsedAndDeparsed(sql); + } + + @Test + public void testAddPartitionDefinitionAst() throws JSQLParserException { + AlterExpressionPartition expression = parseExpression( + "ALTER TABLE sales ADD PARTITION " + + "(PARTITION p2027 VALUES LESS THAN (2028) ENGINE = InnoDB " + + "COMMENT = 'archive')"); + + assertEquals(AlterOperation.ADD_PARTITION, expression.getOperation()); + PartitionDefinition definition = expression.getPartitionDefinitions().get(0); + assertEquals("p2027", definition.getPartitionName()); + assertEquals(PartitionDefinition.ValueOperator.LESS_THAN, + definition.getValueOperator()); + assertEquals(List.of("2028"), definition.getValues()); + assertEquals("InnoDB", definition.getStorageEngine()); + assertEquals("'archive'", definition.getComment()); + } + + @Test + public void testAllPartitionsAndCoalesceCountAst() throws JSQLParserException { + AlterExpressionPartition all = + parseExpression("ALTER TABLE sales TRUNCATE PARTITION ALL"); + assertTrue(all.isAllPartitions()); + assertEquals(List.of("ALL"), all.getPartitionNames()); + assertNull(all.getCoalescePartitionCount()); + + AlterExpressionPartition coalesce = + parseExpression("ALTER TABLE sales COALESCE PARTITION 2"); + assertFalse(coalesce.isAllPartitions()); + assertEquals(2, coalesce.getCoalescePartitionCount()); + assertEquals(2, coalesce.getCoalescePartitionNumber()); + } + + @Test + public void testAddPartitionCountAst() throws JSQLParserException { + AlterExpressionPartition expression = + parseExpression("ALTER TABLE clients ADD PARTITION PARTITIONS 6"); + + assertEquals(AlterOperation.ADD_PARTITION, expression.getOperation()); + assertEquals(6, expression.getPartitionCount()); + assertNull(expression.getPartitionDefinitions()); + assertNull(expression.getModifier()); + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE clients ADD PARTITION LOCAL PARTITIONS 6", + parser -> parser.withDialect(Dialect.MYSQL)); + AlterExpressionPartition local = assertInstanceOf(AlterExpressionPartition.class, + alter.getAlterExpressions().get(0)); + assertEquals(6, local.getPartitionCount()); + assertEquals(Modifier.LOCAL, local.getModifier()); + } + + @Test + public void testBinlogModifierAst() throws JSQLParserException { + AlterExpressionPartition coalesce = + parseExpression("ALTER TABLE clients COALESCE PARTITION NO_WRITE_TO_BINLOG 2"); + assertEquals(Modifier.NO_WRITE_TO_BINLOG, coalesce.getModifier()); + assertEquals(2, coalesce.getCoalescePartitionCount()); + + AlterExpressionPartition analyze = + parseExpression("ALTER TABLE sales ANALYZE PARTITION LOCAL ALL"); + assertEquals(Modifier.LOCAL, analyze.getModifier()); + assertTrue(analyze.isAllPartitions()); + + AlterExpressionPartition definitions = parseExpression( + "ALTER TABLE sales ADD PARTITION NO_WRITE_TO_BINLOG " + + "(PARTITION p2027 VALUES LESS THAN (2028))"); + assertEquals(Modifier.NO_WRITE_TO_BINLOG, definitions.getModifier()); + assertNull(definitions.getPartitionCount()); + assertEquals("p2027", definitions.getPartitionDefinitions().get(0).getPartitionName()); + } + + @Test + public void testLocalRemainsAPartitionNameWithoutFollowingNames() + throws JSQLParserException { + AlterExpressionPartition single = + parseExpression("ALTER TABLE sales ANALYZE PARTITION local"); + assertNull(single.getModifier()); + assertEquals(List.of("local"), single.getPartitionNames()); + + AlterExpressionPartition list = + parseExpression("ALTER TABLE sales ANALYZE PARTITION local, p1"); + assertNull(list.getModifier()); + assertEquals(List.of("local", "p1"), list.getPartitionNames()); + + AlterExpressionPartition reorganize = parseExpression( + "ALTER TABLE sales REORGANIZE PARTITION local INTO " + + "(PARTITION p0 VALUES LESS THAN (100))"); + assertNull(reorganize.getModifier()); + assertEquals(List.of("local"), reorganize.getPartitionNames()); + } + + @Test + public void testAddPartitionFormsRequireAdd() { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "ALTER TABLE sales MODIFY PARTITION (PARTITION p3 VALUES LESS THAN (2002))")); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "ALTER TABLE sales ALTER PARTITION (PARTITION p3 VALUES LESS THAN (2002))")); + } + + @Test + public void testLegacyCoalesceAccessorsShareThePartitionCount() { + AlterExpressionPartition expression = new AlterExpressionPartition() + .withOperation(AlterOperation.COALESCE_PARTITION); + assertEquals("COALESCE PARTITION 0", expression.toString()); + + expression.setCoalescePartitionNumber(3); + assertEquals(3, expression.getPartitionCount()); + assertEquals(3, expression.getCoalescePartitionCount()); + assertEquals("COALESCE PARTITION 3", expression.toString()); + + expression.setPartitionCount(4); + assertEquals(4, expression.getCoalescePartitionNumber()); + expression.setCoalescePartitionCount(null); + assertEquals(0, expression.getCoalescePartitionNumber()); + } + + @Test + public void testExchangePartitionAst() throws JSQLParserException { + AlterExpressionPartition expression = parseExpression( + "ALTER TABLE sales EXCHANGE PARTITION `p0` WITH TABLE " + + "`archive`.`sales_2024` WITHOUT VALIDATION"); + + assertEquals(List.of("`p0`"), expression.getPartitionNames()); + assertEquals("`archive`.`sales_2024`", + expression.getExchangeTable().getFullyQualifiedName()); + assertEquals(AlterExpressionPartition.ExchangeValidationMode.WITHOUT, + expression.getExchangeValidationMode()); + assertTrue(expression.isExchangePartitionWithoutValidation()); + assertFalse(expression.isExchangePartitionWithValidation()); + assertEquals("`archive`.`sales_2024`", expression.getExchangePartitionTableName()); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE sales " + expression); + } + + @Test + public void testPartitionByAst() throws JSQLParserException { + AlterExpressionPartition expression = parseExpression( + "ALTER TABLE sales PARTITION BY LIST COLUMNS (region, tier) " + + "(PARTITION p_eu VALUES IN (('eu', 1), ('eu', 2)), " + + "PARTITION p_us VALUES IN (('us', 1)))"); + + TablePartitioning partitioning = expression.getPartitioning(); + assertEquals(TablePartitioning.Type.LIST, partitioning.getType()); + assertTrue(partitioning.isColumnsSyntax()); + assertEquals("region", partitioning.getColumns().get(0).getColumnName()); + assertEquals("tier", partitioning.getColumns().get(1).getColumnName()); + assertEquals(PartitionDefinition.ValueOperator.IN, + partitioning.getPartitionDefinitions().get(0).getValueOperator()); + + // Legacy accessors remain populated for source compatibility. + assertEquals("LIST", expression.getPartitionType()); + assertEquals(List.of("region", "tier"), expression.getPartitionColumns()); + assertEquals(partitioning.getPartitionDefinitions(), expression.getPartitionDefinitions()); + } + + @ParameterizedTest + @MethodSource("partitionOptionsAfterActions") + public void testPartitionOptionsEndTheActionListWithoutComma(String sql, + List operations) throws JSQLParserException { + // with the MySQL dialect and without a dialect + for (Consumer mode : List.>of( + parser -> parser.withDialect(Dialect.MYSQL), parser -> { + })) { + Alter alter = + assertInstanceOf(Alter.class, assertSqlCanBeParsedAndDeparsed(sql, true, mode)); + + assertEquals(operations, alter.getAlterExpressions().stream() + .map(AlterExpression::getOperation).collect(Collectors.toList())); + } + } + + static Stream partitionOptionsAfterActions() { + return Stream.of( + Arguments.of("ALTER TABLE t1 ADD COLUMN d INT PARTITION BY HASH (id) PARTITIONS 4", + List.of(AlterOperation.ADD, AlterOperation.PARTITION_BY)), + Arguments.of("ALTER TABLE t1 ENGINE = InnoDB REMOVE PARTITIONING", + List.of(AlterOperation.ENGINE, AlterOperation.REMOVE_PARTITIONING)), + Arguments.of("ALTER TABLE t1 ENGINE = InnoDB PARTITION BY KEY (id) PARTITIONS 2", + List.of(AlterOperation.ENGINE, AlterOperation.PARTITION_BY)), + Arguments.of("ALTER TABLE t1 ALGORITHM = INPLACE, ADD COLUMN x INT NOT NULL " + + "DEFAULT 0 PARTITION BY KEY (id) PARTITIONS 2", + List.of(AlterOperation.ALGORITHM, AlterOperation.ADD, + AlterOperation.PARTITION_BY)), + Arguments.of("ALTER TABLE t1 MODIFY COLUMN d BIGINT REMOVE PARTITIONING", + List.of(AlterOperation.MODIFY, AlterOperation.REMOVE_PARTITIONING)), + Arguments.of( + "ALTER TABLE t1 CHANGE COLUMN d e INT AFTER id PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (10), " + + "PARTITION p1 VALUES LESS THAN MAXVALUE)", + List.of(AlterOperation.CHANGE, AlterOperation.PARTITION_BY)), + Arguments.of("ALTER TABLE t1 ADD INDEX i (a) PARTITION BY HASH (id)", + List.of(AlterOperation.ADD, AlterOperation.PARTITION_BY)), + Arguments.of("ALTER TABLE t1 DROP COLUMN c REMOVE PARTITIONING", + List.of(AlterOperation.DROP, AlterOperation.REMOVE_PARTITIONING))); + } + + @Test + public void testColumnDefinitionStopsBeforePartitionOptions() throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t1 ADD COLUMN d INT PARTITION BY HASH (id) PARTITIONS 4", + parser -> parser.withDialect(Dialect.MYSQL)); + + AlterExpression add = alter.getAlterExpressions().get(0); + assertEquals("d INT", add.getColDataTypeList().get(0).toString()); + assertTrue(add.getColDataTypeList().get(0).getColumnOptions().isEmpty()); + AlterExpressionPartition partitionBy = + assertInstanceOf(AlterExpressionPartition.class, + alter.getAlterExpressions().get(1)); + assertEquals(TablePartitioning.Type.HASH, partitionBy.getPartitioning().getType()); + assertEquals(4L, partitionBy.getPartitioning().getPartitions()); + } + + @Test + public void testPartitionOptionsAreNotPrecededByComma() throws JSQLParserException { + // MySQL rejects a comma before PARTITION BY; the rendering drops it + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t1 ADD COLUMN d INT, PARTITION BY HASH (id)"); + assertEquals("ALTER TABLE t1 ADD COLUMN d INT PARTITION BY HASH (id)", alter.toString()); + assertSqlCanBeParsedAndDeparsed(alter.toString(), true); + assertEquals(" ", Alter.separatorBefore(alter.getAlterExpressions().get(1))); + assertEquals(", ", Alter.separatorBefore(alter.getAlterExpressions().get(0))); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "ALTER TABLE t1 PARTITION BY HASH (id) REMOVE PARTITIONING", + parser -> parser.withDialect(Dialect.MYSQL))); + } + + @Test + public void testProgrammaticConstruction() { + AlterExpressionPartition expression = new AlterExpressionPartition() + .withOperation(AlterOperation.EXCHANGE_PARTITION) + .addPartitionNames("p0") + .withExchangeTable(new Table("archive", "sales_2024")) + .withExchangeValidationMode( + AlterExpressionPartition.ExchangeValidationMode.WITHOUT); + + assertEquals("EXCHANGE PARTITION p0 WITH TABLE archive.sales_2024 WITHOUT VALIDATION", + expression.toString()); + + AlterExpressionPartition add = new AlterExpressionPartition() + .withOperation(AlterOperation.ADD_PARTITION) + .withModifier(Modifier.NO_WRITE_TO_BINLOG) + .withPartitionCount(4); + assertEquals("ADD PARTITION NO_WRITE_TO_BINLOG PARTITIONS 4", add.toString()); + } + + private static AlterExpressionPartition parseExpression(String sql) + throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse(sql); + return assertInstanceOf(AlterExpressionPartition.class, + alter.getAlterExpressions().get(0)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterConstraintTest.java new file mode 100644 index 0000000000..4946a3aa20 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterConstraintTest.java @@ -0,0 +1,120 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlAlterConstraintTest { + @ParameterizedTest + @ValueSource(strings = { + "CONSTRAINT uq UNIQUE KEY idx (id)", + "CONSTRAINT uq UNIQUE INDEX idx USING BTREE (id DESC) COMMENT 'unique id'", + "CONSTRAINT `unique id` UNIQUE KEY `index id` (id)", + "CONSTRAINT UNIQUE KEY idx (id)", + "CONSTRAINT uq UNIQUE (id)", + "UNIQUE KEY idx (id)", + "CONSTRAINT pk PRIMARY KEY (id)", + "CONSTRAINT PRIMARY KEY (id)", + "CHECK (id > 0)", + "CONSTRAINT CHECK (id > 0) NOT ENFORCED", + "CONSTRAINT positive CHECK (id > 0) ENFORCED", + "CONSTRAINT fk FOREIGN KEY parent_idx (id) REFERENCES parent (id) ON DELETE CASCADE" + }) + void createAndAlterExposeTheSameConstraint(String definition) throws JSQLParserException { + CreateTable create = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT, " + definition + ")", p -> p.withDialect(Dialect.MYSQL)); + Alter alter = parse("ALTER TABLE t ADD " + definition + ", ADD COLUMN other INT"); + NamedConstraint actual = alter.getAlterExpressions().get(0).getConstraint(); + assertEquals(create.getTableConstraints().get(0).getClass(), actual.getClass()); + assertEquals(create.getTableConstraints().get(0).toString(), actual.toString()); + assertEquals(2, alter.getAlterExpressions().size()); + assertRoundTrip(alter); + } + + @Test + void constraintAndIndexNamesCanBeEditedIndependently() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE KEY idx (id)"); + KeyConstraint unique = assertInstanceOf(KeyConstraint.class, + alter.getAlterExpressions().get(0).getConstraint()); + assertEquals("uq", unique.getName()); + assertEquals("idx", unique.getIndexName()); + assertEquals(ConstraintKind.UNIQUE, unique.getKind()); + unique.setName("new_constraint"); + unique.setIndexName("new_index"); + assertEquals("ALTER TABLE t ADD CONSTRAINT new_constraint UNIQUE KEY new_index (id)", + alter.toString()); + assertRoundTrip(alter); + } + + @Test + void unnamedCheckRetainsKeywordEnforcementAndExpressionVisitor() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT CHECK (id > 0) NOT ENFORCED"); + CheckConstraint check = assertInstanceOf(CheckConstraint.class, + alter.getAlterExpressions().get(0).getConstraint()); + assertTrue(check.isUseConstraintKeyword()); + assertEquals(Boolean.FALSE, check.getEnforced()); + StringBuilder buffer = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(10); + } + }; + alter.accept(new StatementDeParser(expressions, new SelectDeParser(), buffer), null); + assertEquals("ALTER TABLE t ADD CONSTRAINT CHECK (id > 10) NOT ENFORCED", + buffer.toString()); + assertRoundTrip(parse(buffer.toString())); + } + + @Test + void sharedIndexStillProvidesLegacyPrimaryKeyColumns() throws JSQLParserException { + AlterExpression primary = parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id)") + .getAlterExpressions().get(0); + assertEquals(List.of("id"), primary.getPkColumns()); + } + + @ParameterizedTest + @ValueSource(strings = {"CHECK", "CHECK ()", "CONSTRAINT CHECK (id > 0) (id < 10)", + "CONSTRAINT uq UNIQUE KEY idx ()"}) + void rejectsIncompleteOrRepeatedConstraintBodies(String definition) { + assertThrows(JSQLParserException.class, () -> parse("ALTER TABLE t ADD " + definition)); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static void assertRoundTrip(Alter alter) throws JSQLParserException { + StringBuilder buffer = new StringBuilder(); + alter.accept(new StatementDeParser(buffer), null); + assertEquals(alter.toString(), buffer.toString()); + assertEquals(alter.toString(), parse(buffer.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterOrderingOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterOrderingOptionsTest.java new file mode 100644 index 0000000000..507a6db0e9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterOrderingOptionsTest.java @@ -0,0 +1,98 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlAlterOrderingOptionsTest { + @ParameterizedTest + @ValueSource(strings = {"ORDER BY id DESC, label ASC", "ORDER BY id, label", + "ENGINE = InnoDB, ROW_FORMAT = DYNAMIC, STATS_PERSISTENT = DEFAULT", + "STATS_AUTO_RECALC = 1, STATS_SAMPLE_PAGES = 16", + "ENGINE = InnoDB, ORDER BY id DESC, label ASC", "ROW_FORMAT COMPACT, ORDER BY id DESC"}) + void actionAndOrderListBoundariesRemainSeparate(String actions) throws JSQLParserException { + Alter alter = parse("ALTER TABLE t " + actions); + assertEquals("ALTER TABLE t " + actions, alter.toString()); + alter.getAlterExpressions().forEach( + action -> assertNotEquals(AlterOperation.UNSPECIFIC, action.getOperation())); + assertRoundTrip(alter); + assertEquals(2, CCJSqlParserUtil + .parseStatements(alter + "; SELECT 1", p -> p.withDialect(Dialect.MYSQL)).size()); + } + + @Test + void orderElementsAreMutableAndVisited() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ORDER BY id DESC, label ASC"); + AlterExpressionOrderBy order = (AlterExpressionOrderBy) alter.getAlterExpressions().get(0); + assertEquals(2, order.getOrderByElements().size()); + order.getOrderByElements().get(0).setExpression(new Column("new_id")); + order.getOrderByElements().get(0).setAsc(true); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(order, visited::add, ignored -> { + }); + assertEquals(2, visited.size()); + assertEquals("ALTER TABLE t ORDER BY new_id ASC, label ASC", alter.toString()); + assertRoundTrip(alter); + } + + @Test + void rowFormatAndStatisticsShareCreateOptions() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ROW_FORMAT = DYNAMIC, STATS_PERSISTENT = DEFAULT"); + TableOption rowFormat = ((AlterExpressionTableOption) alter.getAlterExpressions().get(0)) + .getStructuredTableOption(); + TableOption statistics = ((AlterExpressionTableOption) alter.getAlterExpressions().get(1)) + .getStructuredTableOption(); + assertEquals(TableOption.Kind.ROW_FORMAT, rowFormat.getKind()); + assertEquals(TableOption.Kind.STATS_PERSISTENT, statistics.getKind()); + rowFormat.setValue("COMPACT"); + statistics.setValue("1"); + assertEquals("ALTER TABLE t ROW_FORMAT = COMPACT, STATS_PERSISTENT = 1", alter.toString()); + CreateTable create = + (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t (id INT) ROW_FORMAT = COMPACT", + p -> p.withDialect(Dialect.MYSQL)); + assertEquals(rowFormat.toString(), + create.getTableOption(TableOption.Kind.ROW_FORMAT).orElseThrow().toString()); + assertRoundTrip(alter); + } + + @Test + void invalidOptionsAndOrderExpressionsFail() { + for (String actions : new String[] {"ROW_FORMAT = invalid", "STATS_PERSISTENT = 2", + "ORDER BY id + 1", "ORDER BY id, ENGINE = InnoDB", + "ORDER BY id, ADD COLUMN x INT"}) { + assertThrows(JSQLParserException.class, () -> parse("ALTER TABLE t " + actions)); + } + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static void assertRoundTrip(Alter alter) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + alter.accept(new StatementDeParser(sql), null); + assertEquals(alter.toString(), sql.toString()); + assertEquals(alter.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlColumnPositionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlColumnPositionTest.java new file mode 100644 index 0000000000..b9408cb654 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlColumnPositionTest.java @@ -0,0 +1,171 @@ +/*- + * #%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.statement.alter; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlColumnPositionTest { + private static final Dialect[] DIALECTS = {null, Dialect.MYSQL}; + + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t1 ADD COLUMN c1 INT NOT NULL DEFAULT 0 COMMENT 'x' AFTER id", + "ALTER TABLE t1 ADD c2 INT FIRST", + "ALTER TABLE t1 ADD COLUMN c5 INT, ADD COLUMN c6 INT AFTER c5, ADD INDEX idx_c5 (c5), " + + "ALGORITHM = INPLACE, LOCK = NONE", + "ALTER TABLE t1 MODIFY COLUMN name VARCHAR (200) CHARACTER SET utf8mb4 " + + "COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT 'n' AFTER id", + "ALTER TABLE t1 CHANGE name name VARCHAR (10) FIRST", + "ALTER TABLE t1 ADD COLUMN geo POINT NOT NULL SRID 4326 AFTER id", + "ALTER TABLE t1 MODIFY c1 INT FIRST, MODIFY c2 INT AFTER `c1`", + "ALTER TABLE t1 CHANGE COLUMN a b INT UNIQUE KEY AFTER c"}) + void roundTripsWithAndWithoutDialect(String sql) throws JSQLParserException { + for (Dialect dialect : DIALECTS) { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed(sql, true, dialect(dialect)); + for (AlterExpression action : alter.getAlterExpressions()) { + if (action.getColDataTypeList() == null) { + continue; + } + for (AlterExpression.ColumnDataType column : action.getColDataTypeList()) { + assertTrue(column.getColumnOptions().stream() + .noneMatch(option -> option.getKind() == ColumnOption.Kind.OTHER), + sql); + } + } + } + } + + @Test + void exposesPositionSeparatelyFromOptions() throws JSQLParserException { + for (Dialect dialect : DIALECTS) { + Alter alter = parse("ALTER TABLE t1 ADD COLUMN c1 INT NOT NULL AFTER `id`, " + + "MODIFY c2 INT FIRST, MODIFY c3 INT", dialect); + AlterExpression.ColumnDataType after = column(alter, 0); + assertEquals(ColumnPosition.Kind.AFTER, after.getPosition().getKind()); + assertEquals("`id`", after.getPosition().getColumnName()); + assertEquals(List.of(ColumnOption.Kind.NULLABILITY), after.getColumnOptions().stream() + .map(ColumnOption::getKind).collect(Collectors.toList())); + assertEquals(List.of("NOT", "NULL", "AFTER", "`id`"), after.getColumnSpecs()); + + AlterExpression.ColumnDataType first = column(alter, 1); + assertEquals(ColumnPosition.first(), first.getPosition()); + assertNull(first.getPosition().getColumnName()); + assertTrue(first.getColumnOptions().isEmpty()); + assertEquals(List.of("FIRST"), first.getColumnSpecs()); + assertNull(column(alter, 2).getPosition()); + assertEquals(List.of(), column(alter, 2).getColumnSpecs()); + } + } + + @Test + void editsRenderAfterOptionsInBothRenderers() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t1 MODIFY c1 INT NOT NULL FIRST", Dialect.MYSQL); + AlterExpression.ColumnDataType column = column(alter, 0); + List snapshot = column.getColumnSpecs(); + assertThrows(UnsupportedOperationException.class, () -> snapshot.add("AFTER")); + + column.setPosition(ColumnPosition.after("c0")); + column.addColumnSpecs("INVISIBLE"); + assertEquals(List.of("NOT", "NULL", "FIRST"), snapshot); + assertEquals(List.of("NOT", "NULL", "INVISIBLE", "AFTER", "c0"), column.getColumnSpecs()); + assertEquals("ALTER TABLE t1 MODIFY c1 INT NOT NULL INVISIBLE AFTER c0", alter.toString()); + assertRoundTrip(alter); + + column.setPosition(null); + assertEquals("ALTER TABLE t1 MODIFY c1 INT NOT NULL INVISIBLE", alter.toString()); + assertRoundTrip(alter); + } + + @Test + void replacingSpecificationsAlsoReplacesThePosition() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t1 ADD COLUMN c1 INT NOT NULL AFTER id", Dialect.MYSQL); + AlterExpression.ColumnDataType column = column(alter, 0); + column.setColumnSpecs(column.getColumnSpecs()); + assertNull(column.getPosition()); + assertEquals(1, column.getColumnOptions().size()); + assertEquals(ColumnOption.Kind.OTHER, column.getColumnOptions().get(0).getKind()); + assertEquals("ALTER TABLE t1 ADD COLUMN c1 INT NOT NULL AFTER id", alter.toString()); + + column.withColumnSpecs(null).withPosition(ColumnPosition.first()); + assertEquals(List.of("FIRST"), column.getColumnSpecs()); + assertEquals("ALTER TABLE t1 ADD COLUMN c1 INT FIRST", alter.toString()); + assertRoundTrip(alter); + } + + @Test + void buildsPositionedDefinitions() throws JSQLParserException { + AlterExpression action = new AlterExpression(); + action.setOperation(AlterOperation.ADD); + action.hasColumn(true); + action.addColDataType(new AlterExpression.ColumnDataType("c1", false, + new ColDataType("INT"), null).withPosition(ColumnPosition.after("id"))); + Alter alter = new Alter().withTable(new Table("t1")).addAlterExpressions(action); + assertEquals("ALTER TABLE t1 ADD COLUMN c1 INT AFTER id", alter.toString()); + assertRoundTrip(alter); + + assertEquals(ColumnPosition.after("id"), ColumnPosition.after("id")); + assertEquals(ColumnPosition.after("id").hashCode(), + ColumnPosition.after("id").hashCode()); + assertNotEquals(ColumnPosition.after("id"), ColumnPosition.first()); + assertThrows(NullPointerException.class, () -> ColumnPosition.after(null)); + assertThrows(UnsupportedOperationException.class, + () -> ColumnPosition.after("id").getTokens().set(0, "BEFORE")); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t1 ADD COLUMN c1 INT AFTER id NOT NULL", + "ALTER TABLE t1 MODIFY c1 INT FIRST AFTER id"}) + void requiresThePositionToEndTheDefinition(String sql) { + for (Dialect dialect : DIALECTS) { + assertThrows(JSQLParserException.class, () -> parse(sql, dialect), sql); + } + } + + private static AlterExpression.ColumnDataType column(Alter alter, int action) { + return alter.getAlterExpressions().get(action).getColDataTypeList().get(0); + } + + private static Consumer dialect(Dialect dialect) { + return parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }; + } + + private static Alter parse(String sql, Dialect dialect) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, dialect(dialect)); + } + + private static void assertRoundTrip(Alter alter) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + alter.accept(new StatementDeParser(sql), null); + assertEquals(alter.toString(), sql.toString()); + for (Dialect dialect : DIALECTS) { + assertEquals(sql.toString(), parse(sql.toString(), dialect).toString()); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlCommonTableOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlCommonTableOptionsTest.java new file mode 100644 index 0000000000..3caea9295c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlCommonTableOptionsTest.java @@ -0,0 +1,163 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlCommonTableOptionsTest { + @ParameterizedTest + @ValueSource(strings = {"ENGINE='InnoDB'", "ENGINE=\"InnoDB\"", "AVG_ROW_LENGTH=100", + "MAX_ROWS=18446744073709551615", "MIN_ROWS 0", "KEY_BLOCK_SIZE=8", + "TABLESPACE=innodb_file_per_table", "TABLESPACE `space name` STORAGE DISK", + "TABLESPACE ts STORAGE MEMORY", "UNION=(db.m1,db.m2)", "UNION=()"}) + void sharesTableOptions(String sql) throws JSQLParserException { + CreateTable create = (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t(id INT) " + sql); + Alter alter = parse(sql + ", ADD COLUMN extra INT"); + AlterExpressionTableOption action = + assertInstanceOf(AlterExpressionTableOption.class, + alter.getAlterExpressions().get(0)); + TableOption option = action.getStructuredTableOption(); + assertNotEquals(TableOption.Kind.OTHER, option.getKind()); + assertEquals(create.getTableOptions().get(0).toString(), option.toString()); + assertEquals(2, alter.getAlterExpressions().size()); + roundTrip(create); + roundTrip(alter); + } + + @Test + void optionPairsKeepActionAndStatementBoundaries() throws JSQLParserException { + List options = List.of("ENGINE='InnoDB'", "ROW_FORMAT=DYNAMIC", + "STATS_PERSISTENT=1", "COMMENT='x,y'", "AVG_ROW_LENGTH=100", + "TABLESPACE=innodb_file_per_table", "UNION=(m1,m2)"); + for (String left : options) { + for (String right : options) { + if (left.equals(right)) { + continue; + } + for (String separator : new String[] {" ", ", "}) { + String sql = "ALTER TABLE t " + left + separator + right + + ", ADD COLUMN z INT, ALGORITHM=DEFAULT, LOCK=DEFAULT"; + Alter alter = (Alter) CCJSqlParserUtil.parse(sql); + assertEquals(5, alter.getAlterExpressions().size(), sql); + assertEquals(AlterOperation.ADD, + alter.getAlterExpressions().get(2).getOperation()); + assertEquals(AlterOperation.ALGORITHM, + alter.getAlterExpressions().get(3).getOperation()); + assertEquals(AlterOperation.LOCK, + alter.getAlterExpressions().get(4).getOperation()); + roundTrip(alter); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + } + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"DEFAULT CHARACTER SET", "CHARACTER SET", "CHAR SET", "CHARSET", + "DEFAULT CHARSET", "DEFAULT CHAR SET"}) + void quotedCharsetAndCollationKeepLegacyAccess(String name) throws JSQLParserException { + Alter alter = parse(name + "='utf8mb4' COLLATE='utf8mb4_bin' ROW_FORMAT=DYNAMIC"); + AlterExpression action = alter.getAlterExpressions().get(0); + assertEquals(AlterOperation.CONVERT, action.getOperation()); + assertEquals("'utf8mb4'", action.getCharacterSet()); + assertEquals("'utf8mb4_bin'", action.getCollation()); + assertEquals(2, alter.getAlterExpressions().size()); + action.setCharacterSet("utf8mb4"); + action.setCollation("utf8mb4_general_ci"); + roundTrip(alter); + } + + @Test + void structuredAndLegacyMutationsUseTheSameValues() throws JSQLParserException { + Alter alter = parse("ENGINE=InnoDB KEY_BLOCK_SIZE=8 COMMENT='old'"); + AlterExpressionTableOption engine = + (AlterExpressionTableOption) alter.getAlterExpressions().get(0); + assertEquals(AlterOperation.ENGINE, engine.getOperation()); + engine.setEngineOption("MyISAM"); + assertEquals("MyISAM", engine.getStructuredTableOption().getValue()); + engine.getStructuredTableOption().setValue("InnoDB"); + assertEquals("InnoDB", engine.getEngineOption()); + AlterExpressionTableOption block = + (AlterExpressionTableOption) alter.getAlterExpressions().get(1); + assertEquals(AlterOperation.KEY_BLOCK_SIZE, block.getOperation()); + block.setKeyBlockSize(4); + assertEquals("4", block.getStructuredTableOption().getValue()); + block.getStructuredTableOption().setValue("16"); + assertEquals(16, block.getKeyBlockSize()); + AlterExpressionTableOption comment = + (AlterExpressionTableOption) alter.getAlterExpressions().get(2); + comment.setCommentText("'new'"); + assertEquals("'new'", comment.getStructuredTableOption().getValue()); + comment.getStructuredTableOption().setUseEquals(false); + assertEquals(AlterOperation.COMMENT, comment.getOperation()); + roundTrip(alter); + } + + @Test + void visitsAndChangesUnionSources() throws JSQLParserException { + Alter alter = parse("UNION=(m1,m2)"); + TableOption option = ((AlterExpressionTableOption) alter.getAlterExpressions().get(0)) + .getStructuredTableOption(); + assertEquals(Set.of("t", "m1", "m2"), new TablesNamesFinder().getTables(alter)); + option.getUnionTables().set(1, new Table("m3")); + assertEquals(Set.of("t", "m1", "m3"), new TablesNamesFinder().getTables(alter)); + assertTrue(alter.toString().contains("UNION = (m1, m3)")); + roundTrip(alter); + } + + @Test + void tablespaceStorageHasStructuredAndLegacyProjections() throws JSQLParserException { + CreateTable create = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t(id INT) TABLESPACE=ts STORAGE DISK"); + TableOption option = create.getTableOptions().get(0); + assertEquals("ts", option.getValue()); + assertEquals(ColumnOption.Storage.DISK, option.getTablespaceStorage()); + option.setValue("new_ts"); + option.setTablespaceStorage(ColumnOption.Storage.MEMORY); + assertEquals(List.of("TABLESPACE", "=", "new_ts", "STORAGE", "MEMORY"), option.getTokens()); + roundTrip(create); + option.setKind(TableOption.Kind.ENGINE); + assertNull(option.getTablespaceStorage()); + } + + @ParameterizedTest + @ValueSource(strings = {"ENGINE=", "AVG_ROW_LENGTH=-1", "MAX_ROWS='1'", "TABLESPACE=", + "TABLESPACE ts STORAGE UNKNOWN", "ENGINE=InnoDB ADD COLUMN z INT", + "ENGINE=InnoDB LOCK=NONE", "ALGORITHM=DEFAULT ROW_FORMAT=DYNAMIC"}) + void rejectsIncompleteOptionsAndMissingActionCommas(String option) { + assertThrows(JSQLParserException.class, () -> parse(option)); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse("ALTER TABLE t " + sql); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), CCJSqlParserUtil.parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlConvertToCharacterSetTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlConvertToCharacterSetTest.java new file mode 100644 index 0000000000..3ea802139f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlConvertToCharacterSetTest.java @@ -0,0 +1,96 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlConvertToCharacterSetTest { + private static final Consumer MYSQL = p -> p.withDialect(Dialect.MYSQL); + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t CONVERT TO CHARSET utf8mb4", + "ALTER TABLE t CONVERT TO CHARSET utf8mb4 COLLATE utf8mb4_bin", + "ALTER TABLE t CONVERT TO CHARACTER SET DEFAULT", + "ALTER TABLE t CONVERT TO CHARACTER SET DEFAULT COLLATE utf8mb4_bin", + "ALTER TABLE t CONVERT TO CHARACTER SET binary", + "ALTER TABLE t CONVERT TO CHARACTER SET binary COLLATE binary", + "ALTER TABLE t CONVERT TO CHARACTER SET 'utf8mb4'", + "ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE 'utf8mb4_bin'", + "ALTER TABLE t CONVERT TO CHARACTER SET `utf8mb4` COLLATE `utf8mb4_bin`", + "ALTER TABLE t CONVERT TO CHARSET utf8mb4, ADD COLUMN c INT", + "ALTER TABLE t DEFAULT CHARSET utf8mb4", + "ALTER TABLE t CHARSET = utf8mb4 COLLATE = utf8mb4_bin"}) + void keepsEveryCharacterSetForm(String sql) throws JSQLParserException { + for (Statement statement : new Statement[] {CCJSqlParserUtil.parse(sql), + CCJSqlParserUtil.parse(sql, MYSQL)}) { + assertEquals(sql, statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(sql, deparsed.toString()); + } + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", MYSQL).size()); + } + + @Test + void exposesCharsetSpellingNameAndCollation() throws JSQLParserException { + AlterExpression action = + parseAction("ALTER TABLE t CONVERT TO CHARSET utf8mb4 COLLATE 'utf8mb4_bin'"); + assertEquals(AlterOperation.CONVERT, action.getOperation()); + assertEquals(AlterExpression.ConvertType.CONVERT_TO, action.getConvertType()); + assertTrue(action.isUseCharsetKeyword()); + assertEquals("utf8mb4", action.getCharacterSet()); + assertEquals("'utf8mb4_bin'", action.getCollation()); + + action.setUseCharsetKeyword(false); + action.setCollation("utf8mb4_0900_ai_ci"); + assertEquals("CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci", + action.toString()); + + AlterExpression standard = parseAction("ALTER TABLE t CONVERT TO CHARACTER SET DEFAULT"); + assertFalse(standard.isUseCharsetKeyword()); + assertEquals("DEFAULT", standard.getCharacterSet()); + assertNull(standard.getCollation()); + } + + @Test + void keepsCharsetSpellingOfTableOptions() throws JSQLParserException { + AlterExpression action = parseAction("ALTER TABLE t DEFAULT CHARSET = utf8mb4"); + assertEquals(AlterExpression.ConvertType.DEFAULT_CHARACTER_SET, action.getConvertType()); + assertTrue(action.isUseCharsetKeyword()); + assertFalse( + parseAction("ALTER TABLE t DEFAULT CHARACTER SET = utf8mb4").isUseCharsetKeyword()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t CONVERT TO CHARSET", + "ALTER TABLE t CONVERT TO utf8mb4", + "ALTER TABLE t CONVERT TO CHARSET = utf8mb4", + "ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE"}) + void rejectsIncompleteForms(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL)); + } + + private static AlterExpression parseAction(String sql) throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse(sql, MYSQL); + assertEquals(1, alter.getAlterExpressions().size()); + return alter.getAlterExpressions().get(0); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlDropConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlDropConstraintTest.java new file mode 100644 index 0000000000..fa35bdf227 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlDropConstraintTest.java @@ -0,0 +1,138 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlDropConstraintTest { + @ParameterizedTest + @ValueSource(strings = {"DROP FOREIGN KEY fk1", "DROP FOREIGN KEY `fk name`", + "DROP CHECK c1", "DROP CHECK `check name`", + "DROP FOREIGN KEY fk1, DROP CHECK c1", + "DROP FOREIGN KEY fk1, ALGORITHM = INPLACE, LOCK = NONE"}) + void roundTripsNamedDrops(String actions) throws JSQLParserException { + Alter alter = parse("ALTER TABLE t " + actions); + assertEquals("ALTER TABLE t " + actions, alter.toString()); + assertRoundTrip(alter); + } + + @Test + void namesAndKindsAreEditableWithoutColumnLists() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t DROP FOREIGN KEY fk1, DROP CHECK c1"); + AlterExpression foreign = alter.getAlterExpressions().get(0); + AlterExpression check = alter.getAlterExpressions().get(1); + assertEquals(AlterOperation.DROP_FOREIGN_KEY, foreign.getOperation()); + assertEquals("fk1", foreign.getConstraintName()); + assertNull(foreign.getPkColumns()); + assertEquals(AlterOperation.DROP_CHECK, check.getOperation()); + assertEquals("c1", check.getConstraintName()); + check.setConstraintName("`new check`"); + foreign.setConstraintName("fk2"); + assertEquals("ALTER TABLE t DROP FOREIGN KEY fk2, DROP CHECK `new check`", + alter.toString()); + assertRoundTrip(alter); + } + + @Test + void constructedActionsUseTheSameRendererAsParsedActions() throws JSQLParserException { + for (AlterOperation operation : List.of(AlterOperation.DROP_FOREIGN_KEY, + AlterOperation.DROP_CHECK)) { + Alter alter = new Alter(); + alter.setTable(new Table("t")); + alter.addAlterExpression(new AlterExpression().withOperation(operation) + .withConstraintName("`target constraint`")); + Alter parsed = parse(alter.toString()); + assertEquals(operation, parsed.getAlterExpressions().get(0).getOperation()); + assertEquals("`target constraint`", + parsed.getAlterExpressions().get(0).getConstraintName()); + assertRoundTrip(alter); + // Editing the operation on either concrete class must have the same effect. + parsed.getAlterExpressions().get(0).setOperation(AlterOperation.DROP_CHECK); + assertEquals("ALTER TABLE t DROP CHECK `target constraint`", parsed.toString()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"DROP FOREIGN KEY (id)", "DROP UNIQUE (a, b)", + "DROP PRIMARY KEY", "DROP CONSTRAINT IF EXISTS ck CASCADE", "DROP (a, b)", + "DROP COLUMN c", "DROP INDEX idx"}) + void preservesDefaultDropForms(String action) throws JSQLParserException { + String sql = "ALTER TABLE t " + action; + Alter alter = (Alter) CCJSqlParserUtil.parse(sql); + assertEquals(sql, alter.toString().trim()); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP FOREIGN KEY fk1", "DROP FOREIGN KEY `fk name`", + "DROP CHECK c1", "DROP FOREIGN KEY fk1, DROP CHECK c1"}) + void acceptsNamedDropsWithoutDialect(String actions) throws JSQLParserException { + String sql = "ALTER TABLE t " + actions; + Alter alter = (Alter) CCJSqlParserUtil.parse(sql); + + assertEquals(sql, alter.toString()); + for (AlterExpression drop : alter.getAlterExpressions()) { + assertNotNull(drop.getConstraintName()); + assertNull(drop.getDropColumns()); + } + assertEquals(sql, CCJSqlParserUtil.parse(alter.toString()).toString()); + } + + @Test + void mariaDbAcceptsNamedForeignKeyButNotCheckDrops() throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse("ALTER TABLE t DROP FOREIGN KEY fk1", + p -> p.withDialect(Dialect.MARIADB)); + assertEquals(AlterOperation.DROP_FOREIGN_KEY, + alter.getAlterExpressions().get(0).getOperation()); + assertEquals("fk1", alter.getAlterExpressions().get(0).getConstraintName()); + + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("ALTER TABLE t DROP CHECK c1", p -> p.withDialect(Dialect.MARIADB))); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP FOREIGN KEY fk1", "DROP CHECK c1"}) + void otherDialectsKeepRejectingMySqlNamedDrops(String action) { + for (Dialect dialect : List.of(Dialect.POSTGRESQL, Dialect.ORACLE, Dialect.SQLSERVER)) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("ALTER TABLE t " + action, p -> p.withDialect(dialect)), + dialect.name()); + } + } + + @Test + void parenthesizedForeignKeyDropKeepsColumnsWithoutDialect() throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse("ALTER TABLE t DROP FOREIGN KEY (a, b)"); + AlterExpression drop = alter.getAlterExpressions().get(0); + + assertEquals(AlterOperation.DROP_FOREIGN_KEY, drop.getOperation()); + assertEquals(List.of("a", "b"), drop.getDropColumns()); + assertNull(drop.getConstraintName()); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static void assertRoundTrip(Alter alter) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + alter.accept(new StatementDeParser(sql), null); + assertEquals(alter.toString(), sql.toString()); + assertEquals(alter.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlQualifiedRenameTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlQualifiedRenameTest.java new file mode 100644 index 0000000000..ef22243bca --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlQualifiedRenameTest.java @@ -0,0 +1,93 @@ +/*- + * #%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.statement.alter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlQualifiedRenameTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE db1.t RENAME TO db2.t2", + "ALTER TABLE db1.t RENAME AS db2.t2", + "ALTER TABLE db1.t RENAME db2.t2", + "ALTER TABLE `db1`.`t` RENAME TO `db2`.`t2`", + "ALTER TABLE db1.t RENAME TO `db.with.dot`.`table.with.dot`", + "ALTER TABLE db1.t RENAME TO db2.t2, ADD COLUMN other INT", + "ALTER TABLE db1.t RENAME TO t2" + }) + void preservesQualifiedTargetsAndRenameKeywords(String sql) throws JSQLParserException { + Alter alter = parse(sql); + assertEquals(sql, alter.toString()); + assertRoundTrip(alter); + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + } + + @Test + void structuredTargetAndLegacyNameStayInSync() throws JSQLParserException { + Alter alter = parse("ALTER TABLE db1.t RENAME TO db2.t2"); + AlterExpression rename = alter.getAlterExpressions().get(0); + assertEquals("db2", rename.getNewTable().getSchemaName()); + assertEquals("t2", rename.getNewTable().getName()); + rename.getNewTable().setName("renamed"); + assertEquals("db2.renamed", rename.getNewTableName()); + assertThat(new TablesNamesFinder().getTables(alter)) + .containsExactlyInAnyOrder("db1.t", "db2.renamed"); + assertRoundTrip(alter); + rename.setNewTableName("legacy_name"); + assertEquals("legacy_name", rename.getNewTable().getName()); + assertNull(rename.getNewTable().getSchemaName()); + assertRoundTrip(alter); + rename.withNewTable(new Table("db3", "replacement")); + assertEquals("db3.replacement", rename.getNewTableName()); + assertRoundTrip(alter); + rename.setNewTableName(null); + assertNull(rename.getNewTable()); + assertNull(rename.getNewTableName()); + } + + @ParameterizedTest + @ValueSource(strings = {"RENAME COLUMN old_name TO new_name", "RENAME INDEX old_idx TO new_idx", + "RENAME KEY old_idx TO new_idx"}) + void doesNotConfuseOtherRenameOperations(String operation) throws JSQLParserException { + Alter alter = parse("ALTER TABLE t " + operation); + assertNull(alter.getAlterExpressions().get(0).getNewTable()); + assertRoundTrip(alter); + } + + @ParameterizedTest + @ValueSource(strings = {"RENAME TO", "RENAME AS", "RENAME TO db2.", "RENAME TO db2.t2."}) + void rejectsIncompleteTargetNames(String operation) { + assertThrows(JSQLParserException.class, () -> parse("ALTER TABLE t " + operation)); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static void assertRoundTrip(Alter alter) throws JSQLParserException { + StringBuilder buffer = new StringBuilder(); + alter.accept(new StatementDeParser(buffer), null); + assertEquals(alter.toString(), buffer.toString()); + assertEquals(alter.toString(), parse(buffer.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlAlterConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlAlterConstraintTest.java new file mode 100644 index 0000000000..d70098e00b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlAlterConstraintTest.java @@ -0,0 +1,104 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlAlterConstraintTest { + @ParameterizedTest + @ValueSource(strings = { + "CONSTRAINT c CHECK (id > 0) NOT VALID", + "CHECK (id > 0) NOT VALID", + "CONSTRAINT c UNIQUE NULLS NOT DISTINCT (id) INCLUDE (other) WITH (fillfactor = 70) " + + "USING INDEX TABLESPACE fast DEFERRABLE INITIALLY DEFERRED", + "UNIQUE (id) NOT DEFERRABLE INITIALLY IMMEDIATE", + "CONSTRAINT c PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED", + "CONSTRAINT c FOREIGN KEY (id) REFERENCES parent(id) ON DELETE CASCADE " + + "DEFERRABLE INITIALLY DEFERRED NOT VALID", + "FOREIGN KEY (id) REFERENCES parent(id) NOT VALID", + "CONSTRAINT c EXCLUDE USING gist ((id + 1) WITH =) WHERE (id > 0) DEFERRABLE", + "EXCLUDE USING gist (id WITH =)" + }) + void sharedConstraintGrammarParsesAndRoundTrips(String definition) + throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD " + definition); + StringBuilder buffer = new StringBuilder(); + alter.accept(new StatementDeParser(buffer), null); + assertEquals(alter.toString(), buffer.toString()); + assertEquals(alter.toString(), parse(buffer.toString()).toString()); + assertEquals(1, alter.getAlterExpressions().size()); + } + + @Test + void constraintAttributesAndPredicatesRemainStructured() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT c CHECK (id > 0) NOT VALID, " + + "ADD CONSTRAINT u UNIQUE (id) DEFERRABLE INITIALLY DEFERRED"); + CheckConstraint check = assertInstanceOf(CheckConstraint.class, + alter.getAlterExpressions().get(0).getConstraint()); + assertTrue(check.getConstraintAttributes().isNotValid()); + NamedConstraint unique = alter.getAlterExpressions().get(1).getConstraint(); + assertEquals(ConstraintAttributes.Initially.DEFERRED, + unique.getConstraintAttributes().getInitially()); + assertEquals(Boolean.TRUE, unique.getConstraintAttributes().getDeferrable()); + ExcludeConstraint exclude = assertInstanceOf(ExcludeConstraint.class, + parse("ALTER TABLE t ADD EXCLUDE USING gist ((id + 1) WITH =) WHERE (id > 0)") + .getAlterExpressions().get(0).getConstraint()); + assertEquals("id > 0", exclude.getExpression().toString()); + } + + @Test + void commonProjectionPreservesLegacyKeyAccessors() throws JSQLParserException { + AlterExpression primary = parse("ALTER TABLE t ADD PRIMARY KEY (id)") + .getAlterExpressions().get(0); + assertThat(primary.getPkColumns()).containsExactly("id"); + AlterExpression foreign = + parse("ALTER TABLE t ADD FOREIGN KEY (id) REFERENCES app.parent(id)") + .getAlterExpressions().get(0); + assertThat(foreign.getFkColumns()).containsExactly("id"); + assertEquals("app", foreign.getFkSourceSchema()); + assertEquals("parent", foreign.getFkSourceTable()); + assertThat(new TablesNamesFinder().getTables( + parse("ALTER TABLE t ADD FOREIGN KEY (id) REFERENCES app.parent(id) NOT VALID"))) + .containsExactlyInAnyOrder("t", "app.parent"); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t ADD CONSTRAINT c CHECK (id > 0) NOT INVALID", + "ALTER TABLE t ADD UNIQUE (id) INITIALLY UNKNOWN", + "ALTER TABLE t ADD EXCLUDE USING gist (id)", + "ALTER TABLE t ADD CONSTRAINT c CHECK (id > 0), ADD" + }) + void invalidConstraintTailsAreRejected(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlCheckNoInheritTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlCheckNoInheritTest.java new file mode 100644 index 0000000000..1c16ba825e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlCheckNoInheritTest.java @@ -0,0 +1,88 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlCheckNoInheritTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t ADD CONSTRAINT ck CHECK (id > 0) NO INHERIT", + "ALTER TABLE t ADD CHECK (id > 0) NO INHERIT NOT VALID", + "ALTER TABLE t ADD CONSTRAINT ck CHECK (id > 0) NO INHERIT NOT VALID, ADD COLUMN extra INT", + "CREATE TABLE t (id INT, CONSTRAINT ck CHECK (id > 0) NO INHERIT)", + "CREATE TABLE t (id INT CHECK (id > 0) NO INHERIT)", + "CREATE TABLE t (id INT CONSTRAINT ck CHECK (id > 0) NO INHERIT)"}) + void roundTripsSharedCreateAndAlterCheck(String sql) throws JSQLParserException { + Statement statement = parse(sql); + assertEquals(sql, statement.toString()); + roundTrip(statement); + } + + @Test + void exposesIndependentInheritanceAndValidationFlags() throws JSQLParserException { + Alter alter = (Alter) parse( + "ALTER TABLE t ADD CONSTRAINT ck CHECK (id > 0) NO INHERIT NOT VALID"); + CheckConstraint constraint = + (CheckConstraint) alter.getAlterExpressions().get(0).getConstraint(); + assertTrue(constraint.isNoInherit()); + assertTrue(constraint.getConstraintAttributes().isNotValid()); + constraint.setNoInherit(false); + constraint.setExpression(CCJSqlParserUtil.parseExpression("id > 10")); + assertEquals("ALTER TABLE t ADD CONSTRAINT ck CHECK (id > 10) NOT VALID", alter.toString()); + roundTrip(alter); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(alter.getAlterExpressions().get(0), visited::add, table -> { + }); + assertEquals(List.of(constraint.getExpression()), visited); + alter.getAlterExpressions().get(0).setConstraint(new CheckConstraint().withName("new_ck") + .withExpression(CCJSqlParserUtil.parseExpression("id > 20")).withNoInherit(true)); + assertEquals("ALTER TABLE t ADD CONSTRAINT new_ck CHECK (id > 20) NO INHERIT", + alter.toString()); + roundTrip(alter); + } + + @Test + void preservesExistingCheckOptions() throws JSQLParserException { + CreateTable table = (CreateTable) parse("CREATE TABLE t (id INT, CHECK (id > 0))"); + assertFalse(((CheckConstraint) table.getTableConstraints().get(0)).isNoInherit()); + for (String suffix : new String[] {"", " ENFORCED", " NOT ENFORCED"}) { + String sql = "CREATE TABLE t (id INT, CHECK (id > 0)" + suffix + ")"; + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + assertEquals(sql, + CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)).toString()); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlColumnReplacementTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlColumnReplacementTest.java new file mode 100644 index 0000000000..efee371014 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlColumnReplacementTest.java @@ -0,0 +1,123 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.AlterDeParser; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlColumnReplacementTest { + @ParameterizedTest + @ValueSource(strings = {"BIGINT", "BIGINT USING id::bigint", + "VARCHAR(20) COLLATE \"C\" USING id::text"}) + void sharesTypeChangePrefixes(String type) throws JSQLParserException { + for (String column : new String[] {"COLUMN ", ""}) { + for (String prefix : new String[] {"TYPE ", "SET DATA TYPE "}) { + String sql = "ALTER TABLE t ALTER " + column + "id " + prefix + type; + Alter table = (Alter) parse(sql); + AlterExpression.ColumnDataType definition = table.getAlterExpressions().get(0) + .getColDataTypeList().get(0); + assertTrue(definition.isWithType()); + assertEquals(prefix.startsWith("SET"), definition.isUseSetData()); + assertNotEquals("SET", definition.getColDataType().getDataType()); + if (type.contains("USING")) { + assertNotNull(definition.getUsingExpression()); + } + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + definition.setUseSetData(!definition.isUseSetData()); + roundTrip(table); + } + } + for (String prefix : new String[] {"TYPE ", "SET DATA TYPE "}) { + roundTrip(parse("ALTER TYPE row_type ALTER ATTRIBUTE id " + prefix + "BIGINT")); + } + } + + @ParameterizedTest + @ValueSource(strings = {"id + 2", "COALESCE(id, 0) * 2", "CASE WHEN id > 0 THEN id ELSE 0 END"}) + void generatedExpressionIsAnActionAndNotADataType(String expression) + throws JSQLParserException { + for (String column : new String[] {"COLUMN ", ""}) { + String sql = "ALTER TABLE t ALTER " + column + "g SET EXPRESSION AS (" + expression + + "), ADD COLUMN extra INT"; + for (Statement statement : List.of(parse(sql), CCJSqlParserUtil.parse(sql))) { + Alter table = (Alter) statement; + RelationAlterAction action = assertInstanceOf(RelationAlterAction.class, + table.getAlterExpressions().get(0)); + assertEquals(RelationAlterAction.ColumnAction.SET_EXPRESSION, + action.getColumnAction()); + assertEquals("g", action.getColumnName()); + assertNull(action.getColDataTypeList()); + assertNotNull(action.getGenerationExpression()); + List expressions = new ArrayList<>(); + TableDefinitionTraversal.visit(action, expressions::add, ignored -> { + }); + assertEquals(List.of(action.getGenerationExpression()), expressions); + action.setGenerationExpression(new LongValue(42)); + assertTrue(table.toString().contains("SET EXPRESSION AS (42)")); + roundTrip(table); + } + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + } + } + + @Test + void replacementUsesTheExpressionDeparser() throws JSQLParserException { + Alter table = (Alter) parse("ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS (1)"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(2); + } + }; + expressions.setBuilder(output); + new AlterDeParser(output, expressions).deParse(table); + assertEquals("ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS (2)", output.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ALTER COLUMN id SET DATA TYPE", + "ALTER TABLE t ALTER COLUMN id SET DATA BIGINT", + "ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS ()", + "ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS id + 2", + "ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS (id+2) STORED", + "ALTER VIEW v ALTER COLUMN g SET EXPRESSION AS (id+2)", + "ALTER INDEX ix ALTER COLUMN 1 SET EXPRESSION AS (id+2)"}) + void rejectsIncompleteOrWrongContextActions(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlColumnStatisticsTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlColumnStatisticsTest.java new file mode 100644 index 0000000000..eadbfa3322 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlColumnStatisticsTest.java @@ -0,0 +1,84 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlColumnStatisticsTest { + @ParameterizedTest + @ValueSource(strings = {"SET STATISTICS DEFAULT", "SET STATISTICS -1", "SET STATISTICS 10000", + "SET (n_distinct = -0.5)", "SET (n_distinct = -1, n_distinct_inherited = 20)", + "RESET (n_distinct)", "RESET (n_distinct, n_distinct_inherited)"}) + void tablesAndMaterializedViewsShareColumnActions(String action) throws JSQLParserException { + for (String object : List.of("TABLE t", "MATERIALIZED VIEW mv")) { + String sql = "ALTER " + object + " ALTER COLUMN id " + action; + Statement statement = parse(sql); + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + } + + @Test + void defaultTargetsAndAttributeValuesAreMutable() throws JSQLParserException { + AlterRelation index = + (AlterRelation) parse("ALTER INDEX ix ALTER COLUMN 1 SET STATISTICS DEFAULT"); + RelationAlterAction action = index.getActions().get(0); + assertTrue(action.isStatisticsDefault()); + assertNull(action.getStatistics()); + action.setStatistics(200L); + assertFalse(action.isStatisticsDefault()); + assertEquals("ALTER INDEX ix ALTER COLUMN 1 SET STATISTICS 200", index.toString()); + assertRoundTrip(index); + action.setStatisticsDefault(true); + assertRoundTrip(index); + Alter table = (Alter) parse("ALTER TABLE t ALTER COLUMN id SET (n_distinct=10)"); + action = (RelationAlterAction) table.getAlterExpressions().get(0); + action.getOptions().get(0).setValue(new LongValue(20)); + List visited = new ArrayList<>(); + action.visitExpressions(visited::add); + assertEquals(List.of(new LongValue(20)), visited); + assertEquals("ALTER TABLE t ALTER COLUMN id SET (n_distinct = 20)", table.toString()); + assertRoundTrip(table); + } + + @Test + void invalidTargetsAndCrossObjectActionsFail() { + for (String sql : new String[] {"ALTER VIEW v ALTER COLUMN id SET STATISTICS DEFAULT", + "ALTER INDEX ix ALTER COLUMN 1 SET (n_distinct=10)", + "ALTER TABLE t ALTER COLUMN id SET (n_distinct)", + "ALTER TABLE t ALTER COLUMN id SET STATISTICS 10001"}) { + assertThrows(JSQLParserException.class, () -> parse(sql), sql); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlConstraintAttributesChangeTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlConstraintAttributesChangeTest.java new file mode 100644 index 0000000000..c9c1d1e1aa --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlConstraintAttributesChangeTest.java @@ -0,0 +1,98 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlConstraintAttributesChangeTest { + @ParameterizedTest + @ValueSource(strings = {"DEFERRABLE", "NOT DEFERRABLE", "INITIALLY DEFERRED", + "INITIALLY IMMEDIATE", "DEFERRABLE INITIALLY DEFERRED", + "INITIALLY DEFERRED DEFERRABLE", "ENFORCED", "NOT ENFORCED", + "DEFERRABLE INITIALLY DEFERRED NOT ENFORCED", + "NOT ENFORCED INITIALLY IMMEDIATE NOT DEFERRABLE"}) + void sharesCreateAddAndAlterAttributes(String attributes) throws JSQLParserException { + String sql = "ALTER TABLE t ALTER CONSTRAINT fk " + attributes + ", ADD COLUMN z INT"; + Alter table = (Alter) parse(sql); + AlterConstraintAttributes change = assertInstanceOf(AlterConstraintAttributes.class, + table.getAlterExpressions().get(0)); + assertEquals(AlterOperation.ALTER, change.getOperation()); + assertEquals("CONSTRAINT", change.getConstraintType()); + assertEquals("fk", change.getConstraintSymbol()); + assertEquals("fk", change.getConstraintName()); + String declaration = "CONSTRAINT fk FOREIGN KEY(id) REFERENCES p(id) " + attributes; + CreateTable create = (CreateTable) parse("CREATE TABLE t(id INT, " + + declaration + ")"); + Alter add = (Alter) parse("ALTER TABLE t ADD " + declaration); + assertEquals(create.getTableConstraints().get(0).getConstraintAttributes().toString(), + change.getAttributes().toString()); + assertEquals( + add.getAlterExpressions().get(0).getConstraint().getConstraintAttributes() + .toString(), + change.getAttributes().toString()); + assertEquals(2, table.getAlterExpressions().size()); + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + } + + @Test + void legacyAndStructuredMutationsStayConsistent() throws JSQLParserException { + Alter table = (Alter) parse("ALTER TABLE t ALTER CONSTRAINT fk ENFORCED"); + AlterConstraintAttributes action = + (AlterConstraintAttributes) table.getAlterExpressions().get(0); + action.setConstraintName("\"new fk\""); + assertEquals("\"new fk\"", action.getConstraintSymbol()); + action.setEnforced(false); + assertEquals(Boolean.FALSE, action.getAttributes().getEnforced()); + action.getAttributes().setDeferrable(true); + action.getAttributes().setInitially(ConstraintAttributes.Initially.DEFERRED); + assertEquals( + "ALTER TABLE t ALTER CONSTRAINT \"new fk\" DEFERRABLE INITIALLY DEFERRED NOT ENFORCED", + table.toString()); + action.getAttributes().setEnforced(true); + assertTrue(action.isEnforced()); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(table)); + roundTrip(table); + } + + @ParameterizedTest + @ValueSource(strings = {"NOT VALID", "DEFERRABLE NOT VALID", "INITIALLY UNKNOWN", + "DEFERRABLE NOT DEFERRABLE", "ENFORCED NOT ENFORCED", + "INITIALLY DEFERRED INITIALLY IMMEDIATE", "DEFERRABLE,"}) + void rejectsWrongOrDuplicateAlterAttributes(String attributes) { + assertThrows(JSQLParserException.class, + () -> parse("ALTER TABLE t ALTER CONSTRAINT fk " + attributes)); + } + + private static net.sf.jsqlparser.statement.Statement parse(String sql) + throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect( + net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect.POSTGRESQL)); + } + + private static void roundTrip(Alter table) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + table.accept(new StatementDeParser(output)); + assertEquals(table.toString(), output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlConstraintUsingIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlConstraintUsingIndexTest.java new file mode 100644 index 0000000000..d606d107d0 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlConstraintUsingIndexTest.java @@ -0,0 +1,112 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.ConstraintUsingIndex; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.metadata.Named; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; +import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation; +import net.sf.jsqlparser.util.validation.validator.AlterValidator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlConstraintUsingIndexTest { + @ParameterizedTest + @ValueSource(strings = {"UNIQUE USING INDEX i", "PRIMARY KEY USING INDEX i", + "CONSTRAINT uq UNIQUE USING INDEX i DEFERRABLE INITIALLY DEFERRED", + "CONSTRAINT pk PRIMARY KEY USING INDEX i NOT DEFERRABLE INITIALLY IMMEDIATE", + "CONSTRAINT \"Unique Name\" UNIQUE USING INDEX \"Index Name\""}) + void roundTripsExistingIndexConstraints(String body) throws JSQLParserException { + String sql = "ALTER TABLE t ADD " + body; + Alter alter = parse(sql); + ConstraintUsingIndex constraint = assertInstanceOf(ConstraintUsingIndex.class, + alter.getAlterExpressions().get(0).getConstraint()); + assertNull(alter.getAlterExpressions().get(0).getIndex()); + assertEquals(body.endsWith("\"Index Name\"") ? "\"Index Name\"" : "i", + constraint.getExistingIndexName()); + assertEquals(sql, alter.toString()); + roundTrip(alter); + } + + @Test + void supportsMutationAndConstruction() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i DEFERRABLE"); + ConstraintUsingIndex constraint = + (ConstraintUsingIndex) alter.getAlterExpressions().get(0).getConstraint(); + assertEquals(Boolean.TRUE, constraint.getConstraintAttributes().getDeferrable()); + constraint.setName("pk"); + constraint.setType("PRIMARY KEY"); + constraint.setExistingIndexName("other_index"); + assertEquals(ConstraintKind.PRIMARY_KEY, constraint.getKind()); + assertEquals( + "ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY USING INDEX other_index DEFERRABLE", + alter.toString()); + roundTrip(alter); + Alter created = new Alter().withTable(new Table("t")); + created.addAlterExpressions(new AlterExpression().withOperation(AlterOperation.ADD) + .withConstraint( + new ConstraintUsingIndex().withName("uq").withExistingIndexName("i"))); + assertEquals("ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i", created.toString()); + roundTrip(created); + } + + @Test + void distinguishesTableAndIndexAndNewConstraint() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i"); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(alter)); + List visited = new ArrayList<>(); + DatabaseMetaDataValidation metadata = named -> { + visited.add(named); + return named.getNamedObject() != NamedObject.constraint; + }; + AlterValidator validator = new AlterValidator(); + validator.setContext(new ValidationContext().setCapabilities(List.of(metadata))); + validator.validate(alter); + assertTrue(validator.getValidationErrors().isEmpty()); + assertTrue(visited.stream() + .anyMatch(n -> n.getNamedObject() == NamedObject.index && "i".equals(n.getFqn()))); + assertTrue(visited.stream().anyMatch( + n -> n.getNamedObject() == NamedObject.constraint && "uq".equals(n.getFqn()))); + } + + @Test + void preservesOrdinaryConstraintsAndActionBoundaries() throws JSQLParserException { + roundTrip(parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE (id)")); + Alter alter = parse("ALTER TABLE t ADD UNIQUE USING INDEX i, ADD COLUMN extra INT"); + assertEquals(2, alter.getAlterExpressions().size()); + roundTrip(alter); + String oracle = "ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) USING INDEX i"; + assertEquals(oracle, CCJSqlParserUtil.parse(oracle).toString()); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Alter alter) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + alter.accept(new StatementDeParser(sql), null); + assertEquals(alter.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlExclusionOperatorTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlExclusionOperatorTest.java new file mode 100644 index 0000000000..91490616b2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlExclusionOperatorTest.java @@ -0,0 +1,94 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.create.table.KeyElement; +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; +import net.sf.jsqlparser.statement.create.table.ExclusionOperator; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlExclusionOperatorTest { + @ParameterizedTest + @ValueSource(strings = {"OPERATOR(pg_catalog.&&)", "OPERATOR(&&)", "OPERATOR(\"My.Schema\".&&)", + "OPERATOR(pg_catalog.=)", "OPERATOR(pg_catalog.-|-)", "OPERATOR(<<=)", "&&"}) + void supportsSharedCreateAndAlterGrammar(String operator) throws JSQLParserException { + for (String sql : new String[] { + "CREATE TABLE t (c INT4RANGE, CONSTRAINT ex EXCLUDE USING gist (c WITH " + operator + + "))", + "ALTER TABLE t ADD CONSTRAINT ex EXCLUDE USING gist (c WITH " + operator + ")"}) { + Statement statement = parse(sql); + assertEquals(sql, statement.toString()); + assertEquals(operator, + constraint(statement).getColumns().get(0).getExclusionOperator()); + roundTrip(statement); + } + } + + @Test + void exposesEditableSchemaAndSymbolAndSupportsLegacySetter() throws JSQLParserException { + Statement statement = parse( + "ALTER TABLE t ADD CONSTRAINT ex EXCLUDE USING gist (c WITH OPERATOR(pg_catalog.&&))"); + KeyElement key = constraint(statement).getColumns().get(0); + ExclusionOperator operator = key.getExclusionOperatorReference(); + assertEquals("pg_catalog", operator.getSchemaName()); + assertEquals("&&", operator.getName()); + assertTrue(operator.isUseOperatorKeyword()); + operator.setSchemaName("\"My.Schema\""); + operator.setName("="); + assertEquals("OPERATOR(\"My.Schema\".=)", key.getExclusionOperator()); + roundTrip(statement); + key.setExclusionOperator("&&"); + assertEquals("&&", key.getExclusionOperator()); + assertNull(key.getExclusionOperatorReference().getSchemaName()); + roundTrip(statement); + key.setExclusionOperator(null); + assertNull(key.getExclusionOperatorReference()); + key.setExclusionOperatorReference(new ExclusionOperator().withSchemaName("pg_catalog") + .withName("&&").withUseOperatorKeyword(true)); + roundTrip(statement); + } + + @Test + void preservesElementOptionsAndMultipleElementsAndDefaultSyntax() throws JSQLParserException { + roundTrip(parse( + "ALTER TABLE t ADD EXCLUDE USING gist (c WITH OPERATOR(pg_catalog.&&), d WITH =) WHERE (c IS NOT NULL) DEFERRABLE INITIALLY DEFERRED" + .replace("\n", ""))); + String legacy = "CREATE TABLE t (c INT4RANGE, EXCLUDE USING gist (c WITH &&))"; + assertEquals(legacy, CCJSqlParserUtil.parse(legacy).toString()); + roundTrip(parse( + "ALTER TABLE t ADD EXCLUDE USING gist ((c) WITH OPERATOR(pg_catalog.&&)), ADD COLUMN extra INT")); + } + + private static ExcludeConstraint constraint(Statement statement) { + return (ExcludeConstraint) (statement instanceof Alter + ? ((Alter) statement).getAlterExpressions().get(0).getConstraint() + : ((CreateTable) statement).getTableConstraints().get(0)); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlNotNullConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlNotNullConstraintTest.java new file mode 100644 index 0000000000..070190bdef --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlNotNullConstraintTest.java @@ -0,0 +1,150 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.NotNullConstraint; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.AlterDeParser; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation; +import net.sf.jsqlparser.util.validation.metadata.Named; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; +import net.sf.jsqlparser.util.validation.validator.AlterValidator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlNotNullConstraintTest { + @ParameterizedTest + @ValueSource(strings = {"NOT NULL id", "CONSTRAINT nn NOT NULL id", + "CONSTRAINT nn NOT NULL id NO INHERIT", "CONSTRAINT \"NN Name\" NOT NULL \"ID\""}) + void roundTripsCreateAndAlter(String body) throws JSQLParserException { + for (String sql : new String[] {"CREATE TABLE t (id INT, " + body + ")", + "ALTER TABLE t ADD " + body + " NOT VALID"}) { + Statement statement = parse(sql); + assertEquals(sql, statement.toString()); + NotNullConstraint constraint = constraint(statement); + assertEquals(ConstraintKind.NOT_NULL, constraint.getKind()); + assertNotNull(constraint.getColumn()); + roundTrip(statement); + } + } + + @Test + void supportsEditingAndConstruction() throws JSQLParserException { + Alter alter = + (Alter) parse("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NO INHERIT NOT VALID"); + NotNullConstraint constraint = constraint(alter); + assertTrue(constraint.isNoInherit()); + assertTrue(constraint.getConstraintAttributes().isNotValid()); + constraint.setName("new_nn"); + constraint.getColumn().setColumnName("other_id"); + constraint.setNoInherit(false); + assertEquals("ALTER TABLE t ADD CONSTRAINT new_nn NOT NULL other_id NOT VALID", + alter.toString()); + roundTrip(alter); + ConstraintAttributes attributes = new ConstraintAttributes(); + attributes.setNotValid(true); + alter.getAlterExpressions().get(0).setConstraint(new NotNullConstraint().withName("nn") + .withColumn(new Column("id")).withNoInherit(true) + .withConstraintAttributes(attributes)); + assertEquals("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NO INHERIT NOT VALID", + alter.toString()); + roundTrip(alter); + assertEquals(ConstraintKind.NOT_NULL, ConstraintKind.fromType("NOT NULL")); + } + + @Test + void visitsTargetColumnAndUsesCustomExpressionDeparser() throws JSQLParserException { + Alter alter = (Alter) parse("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id"); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(alter.getAlterExpressions().get(0), visited::add, table -> { + }); + assertEquals(List.of(constraint(alter).getColumn()), visited); + CreateTable table = (CreateTable) parse("CREATE TABLE t (id INT, NOT NULL id)"); + visited.clear(); + TableDefinitionTraversal.visit(table, visited::add, source -> { + }); + assertEquals(List.of(constraint(table).getColumn()), visited); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(alter)); + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("replacement"); + } + }; + expressions.setBuilder(sql); + new AlterDeParser(sql, expressions).deParse(alter); + assertEquals("ALTER TABLE t ADD CONSTRAINT nn NOT NULL replacement", sql.toString()); + } + + @Test + void validatesNewConstraintAndExistingColumn() throws JSQLParserException { + List visited = new ArrayList<>(); + DatabaseMetaDataValidation metadata = named -> { + visited.add(named); + return named.getNamedObject() != NamedObject.constraint; + }; + AlterValidator validator = new AlterValidator(); + validator.setContext(new ValidationContext().setCapabilities(List.of(metadata))); + validator.validate((Alter) parse("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NOT VALID")); + assertTrue(validator.getValidationErrors().isEmpty()); + assertTrue(visited.stream().anyMatch( + n -> n.getNamedObject() == NamedObject.column && "id".equals(n.getFqn()))); + assertTrue(visited.stream().anyMatch( + n -> n.getNamedObject() == NamedObject.constraint && "nn".equals(n.getFqn()))); + assertTrue(visited.stream().noneMatch(n -> n.getNamedObject() == NamedObject.index)); + } + + @Test + void preservesColumnNullabilityAndActionBoundaries() throws JSQLParserException { + roundTrip(parse("ALTER TABLE t ADD NOT NULL id NOT VALID, ADD COLUMN extra INT")); + for (String sql : new String[] {"CREATE TABLE t (id INT NOT NULL)", + "ALTER TABLE t ALTER COLUMN id SET NOT NULL", + "ALTER TABLE t ALTER COLUMN id DROP NOT NULL"}) { + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + roundTrip(parse(sql)); + } + } + + private static NotNullConstraint constraint(Statement statement) { + return (NotNullConstraint) (statement instanceof Alter + ? ((Alter) statement).getAlterExpressions().get(0).getConstraint() + : ((CreateTable) statement).getTableConstraints().get(0)); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java new file mode 100644 index 0000000000..e7d10cddef --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java @@ -0,0 +1,259 @@ +/*- + * #%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.statement.alter; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlRelationAlterTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t CLUSTER ON ix", "ALTER TABLE t SET WITHOUT CLUSTER", + "ALTER TABLE t SET WITHOUT OIDS", "ALTER TABLE t SET LOGGED", + "ALTER TABLE t SET UNLOGGED", + "ALTER TABLE t OF public.typ", "ALTER TABLE t NOT OF", + "ALTER TABLE t ENABLE ALWAYS TRIGGER trg", "ALTER TABLE t ENABLE REPLICA TRIGGER trg", + "ALTER TABLE t DISABLE TRIGGER ALL", "ALTER TABLE t ENABLE TRIGGER USER", + "ALTER TABLE t ENABLE TRIGGER \"ALL\"", + "ALTER MATERIALIZED VIEW mv CLUSTER ON ix", + "ALTER MATERIALIZED VIEW mv SET WITHOUT CLUSTER", + "ALTER INDEX ix RENAME TO ix2", + "ALTER INDEX ix SET TABLESPACE pg_default", + "ALTER INDEX ix SET (fillfactor=80)", + "ALTER INDEX ix RESET (fillfactor)", + "ALTER INDEX ix ALTER COLUMN 1 SET STATISTICS 200", + "ALTER INDEX ix ALTER 1 SET STATISTICS 200", + "ALTER INDEX IF EXISTS ix RENAME TO ix2", + "ALTER INDEX IF EXISTS ix SET TABLESPACE pg_default", + "ALTER INDEX IF EXISTS ix SET (fillfactor=80)", + "ALTER INDEX IF EXISTS ix RESET (fillfactor)", + "ALTER INDEX IF EXISTS ix ALTER COLUMN 1 SET STATISTICS 200", + "ALTER INDEX IF EXISTS ix ALTER 1 SET STATISTICS 200", + "ALTER INDEX ix ATTACH PARTITION child_ix", + "ALTER VIEW v ALTER COLUMN id SET DEFAULT 1", + "ALTER VIEW v ALTER COLUMN id DROP DEFAULT", + "ALTER VIEW v SET (security_invoker=true)", + "ALTER VIEW v RESET (security_invoker)", + "ALTER VIEW v OWNER TO CURRENT_USER", + "ALTER VIEW v RENAME COLUMN id TO new_id", + "ALTER VIEW v RENAME TO v2", + "ALTER VIEW v SET SCHEMA ddl_aux", + "ALTER MATERIALIZED VIEW mv RENAME TO mv2", + "ALTER MATERIALIZED VIEW mv RENAME COLUMN id TO new_id", + "ALTER MATERIALIZED VIEW mv OWNER TO CURRENT_USER", + "ALTER MATERIALIZED VIEW mv SET SCHEMA ddl_aux", + "ALTER MATERIALIZED VIEW mv SET (fillfactor=80)", + "ALTER MATERIALIZED VIEW mv RESET (fillfactor)", + "ALTER MATERIALIZED VIEW mv SET TABLESPACE pg_default", + "ALTER MATERIALIZED VIEW mv SET ACCESS METHOD heap", + "ALTER MATERIALIZED VIEW mv ALTER COLUMN id SET STATISTICS 200", + "ALTER MATERIALIZED VIEW mv ALTER COLUMN label SET STORAGE EXTENDED", + "ALTER TABLE t ALTER COLUMN gen DROP EXPRESSION IF EXISTS", + "ALTER TABLE t ALTER CONSTRAINT ck INHERIT", + "ALTER TABLE t ALTER CONSTRAINT ck NO INHERIT", + "ALTER TABLE t ALTER CONSTRAINT nn INHERIT", + "ALTER TABLE t ALTER CONSTRAINT nn NO INHERIT", + "ALTER TABLE t VALIDATE CONSTRAINT ck", + "ALTER TABLE t SET (toast.autovacuum_enabled=false,fillfactor=70)", + "ALTER TABLE t RESET(toast.autovacuum_enabled,fillfactor)", + "ALTER TABLE t INHERIT p", + "ALTER TABLE t NO INHERIT p", + "ALTER TABLE t REPLICA IDENTITY NOTHING", + "ALTER TABLE t REPLICA IDENTITY USING INDEX ix", + "ALTER TABLE t SET ACCESS METHOD heap"}) + void auditedActionsAreStructuredAndRoundTrip(String sql) throws JSQLParserException { + Statement statement = parse(sql); + if (statement instanceof Alter) { + assertInstanceOf(RelationAlterAction.class, + ((Alter) statement).getAlterExpressions().get(0)); + } else { + AlterRelation relation = assertInstanceOf(AlterRelation.class, statement); + assertFalse(relation.getActions().isEmpty()); + } + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @ParameterizedTest + @ValueSource(strings = {"DEFAULT", "FULL", "NOTHING"}) + void replicaIdentityModesAreStructuredAndEditable(String mode) throws JSQLParserException { + Alter statement = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t REPLICA IDENTITY " + mode, false, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + RelationAlterAction action = assertInstanceOf(RelationAlterAction.class, + statement.getAlterExpressions().get(0)); + assertEquals(RelationAlterAction.Kind.REPLICA_IDENTITY, action.getKind()); + assertEquals(RelationAlterAction.ReplicaIdentity.valueOf(mode), + action.getReplicaIdentity()); + assertRoundTrip(statement); + + action.setReplicaIdentity(RelationAlterAction.ReplicaIdentity.NOTHING); + assertEquals("ALTER TABLE t REPLICA IDENTITY NOTHING", statement.toString()); + assertRoundTrip(statement); + } + + @Test + void indexNameAndActionParametersCanBeReplaced() throws JSQLParserException { + AlterRelation index = + (AlterRelation) parse("ALTER INDEX IF EXISTS ix ALTER COLUMN 1 SET STATISTICS 200"); + assertEquals(AlterRelation.ObjectType.INDEX, index.getObjectType()); + assertTrue(index.isIfExists()); + assertEquals(1, index.getActions().get(0).getColumnNumber()); + index.setRelation(new Table("new_ix")); + index.getActions().get(0).setStatistics(100L); + assertEquals("ALTER INDEX IF EXISTS new_ix ALTER COLUMN 1 SET STATISTICS 100", + index.toString()); + assertTrue(new TablesNamesFinder().getTables(index).isEmpty()); + assertRoundTrip(index); + assertEquals("INDEX", index.accept(new StatementVisitorAdapter() { + @Override + public String visit(AlterRelation statement, S context) { + return statement.getObjectType().name(); + } + }, null)); + } + + @Test + void tableInheritanceAndGeneratedExpressionFlagsAreEditable() throws JSQLParserException { + Alter inherit = (Alter) parse("ALTER TABLE t NO INHERIT p"); + RelationAlterAction action = (RelationAlterAction) inherit.getAlterExpressions().get(0); + action.setNoInherit(false); + action.setRelation(new Table("parent")); + assertEquals("ALTER TABLE t INHERIT parent", inherit.toString()); + assertEquals(java.util.Set.of("t", "parent"), new TablesNamesFinder().getTables(inherit)); + Alter drop = + (Alter) parse("ALTER TABLE t ALTER COLUMN generated DROP EXPRESSION IF EXISTS"); + action = (RelationAlterAction) drop.getAlterExpressions().get(0); + assertEquals(RelationAlterAction.ColumnAction.DROP_EXPRESSION, action.getColumnAction()); + assertTrue(action.isUsingIfExists()); + action.setUsingIfExists(false); + assertEquals("ALTER TABLE t ALTER COLUMN generated DROP EXPRESSION", drop.toString()); + assertRoundTrip(drop); + } + + @Test + void viewDefaultsAndTableParametersAreVisited() throws JSQLParserException { + AlterRelation view = (AlterRelation) parse("ALTER VIEW v ALTER COLUMN id SET DEFAULT 1"); + RelationAlterAction action = view.getActions().get(0); + action.setDefaultExpression(new LongValue(2)); + assertEquals("ALTER VIEW v ALTER COLUMN id SET DEFAULT 2", view.toString()); + List visited = new ArrayList<>(); + action.visitExpressions(visited::add); + assertEquals(List.of(action.getDefaultExpression()), visited); + assertEquals(java.util.Set.of("v"), new TablesNamesFinder().getTables(view)); + assertRoundTrip(view); + Alter table = + (Alter) parse("ALTER TABLE t SET (toast.autovacuum_enabled=false, fillfactor=70)"); + action = (RelationAlterAction) table.getAlterExpressions().get(0); + assertEquals("toast.autovacuum_enabled", action.getOptions().get(0).getName()); + action.getOptions().get(1).setValue(new LongValue(80)); + visited.clear(); + TableDefinitionTraversal.visit(action, visited::add, ignored -> { + }); + assertEquals(2, visited.size()); + assertTrue(table.toString().endsWith("fillfactor = 80)")); + assertRoundTrip(table); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER INDEX ix OWNER TO me", "ALTER VIEW v SET TABLESPACE pg_default", + "ALTER MATERIALIZED VIEW v ALTER COLUMN id SET DEFAULT 1", + "ALTER INDEX ix ALTER COLUMN id SET STATISTICS 10", + "ALTER TABLE t ALTER COLUMN 1 SET STATISTICS 10", + "ALTER VIEW v RENAME TO v2, SET (security_invoker=true)"}) + void objectBoundariesAreEnforced(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void triggerTargetsAndTypeNamesAreMutableWithoutInventingTables() throws JSQLParserException { + Alter statement = (Alter) parse("ALTER TABLE t ENABLE REPLICA TRIGGER trg"); + RelationAlterAction action = (RelationAlterAction) statement.getAlterExpressions().get(0); + assertEquals(RelationAlterAction.EnableState.ENABLE_REPLICA, action.getEnableState()); + action.setEnableState(RelationAlterAction.EnableState.DISABLE); + action.setTriggerTarget(RelationAlterAction.TriggerTarget.USER); + assertEquals("ALTER TABLE t DISABLE TRIGGER USER", statement.toString()); + assertRoundTrip(statement); + statement = (Alter) parse("ALTER TABLE t OF public.typ"); + action = (RelationAlterAction) statement.getAlterExpressions().get(0); + action.setValue("other.typ"); + assertEquals(java.util.Set.of("t"), new TablesNamesFinder().getTables(statement)); + assertRoundTrip(statement); + for (String sql : List.of("ALTER TABLE t ENABLE ALWAYS TRIGGER ALL", + "ALTER INDEX ix SET LOGGED", "ALTER VIEW v CLUSTER ON ix")) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + } + + @ParameterizedTest + @ValueSource(strings = {"TABLE", "INDEX", "MATERIALIZED VIEW"}) + void bulkTablespaceMovesShareOneModel(String objectType) throws JSQLParserException { + AlterTablespaceMove statement = (AlterTablespaceMove) parse("ALTER " + objectType + + " ALL IN TABLESPACE old_space OWNED BY alice, \"Bob\" SET TABLESPACE new_space NOWAIT"); + assertEquals(objectType.replace(' ', '_'), statement.getObjectType().name()); + assertEquals(List.of("alice", "\"Bob\""), statement.getOwners()); + assertEquals("old_space", statement.getSourceTablespace()); + assertTrue(statement.isNoWait()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + statement.setTargetTablespace("pg_default"); + statement.getOwners().clear(); + statement.setNoWait(false); + assertEquals( + "ALTER " + objectType + " ALL IN TABLESPACE old_space SET TABLESPACE pg_default", + statement.toString()); + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(statement + "; SELECT 1").size()); + } + + @Test + void validateConstraintIsStructuredWithoutThePostgreSqlDialect() throws JSQLParserException { + for (Dialect dialect : new Dialect[] {null, Dialect.ANSI_SQL, Dialect.MYSQL}) { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t ADD CHECK (c > 0) NOT VALID, VALIDATE CONSTRAINT ck", true, + p -> { + if (dialect != null) { + p.withDialect(dialect); + } + }); + RelationAlterAction validate = assertInstanceOf(RelationAlterAction.class, + alter.getAlterExpressions().get(1)); + assertEquals(RelationAlterAction.Kind.VALIDATE_CONSTRAINT, validate.getKind()); + assertEquals("ck", validate.getConstraintName()); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRuleStateTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRuleStateTest.java new file mode 100644 index 0000000000..475a5e5238 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRuleStateTest.java @@ -0,0 +1,71 @@ +/*- + * #%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.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlRuleStateTest { + @ParameterizedTest + @ValueSource(strings = {"ENABLE", "DISABLE", "ENABLE ALWAYS", "ENABLE REPLICA"}) + void ruleAndTriggerActionsShareStates(String state) throws JSQLParserException { + Alter rule = (Alter) CCJSqlParserUtil.parse("ALTER TABLE t " + state + + " RULE \"my rule\", ADD COLUMN z INT"); + RelationAlterAction action = assertInstanceOf(RelationAlterAction.class, + rule.getAlterExpressions().get(0)); + assertEquals(RelationAlterAction.Kind.RULE_STATE, action.getKind()); + assertEquals(state.replace(' ', '_'), action.getEnableState().name()); + assertEquals("\"my rule\"", action.getValue()); + assertEquals(2, rule.getAlterExpressions().size()); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(rule)); + action.setEnableState(RelationAlterAction.EnableState.DISABLE); + action.setValue("new_rule"); + StringBuilder output = new StringBuilder(); + rule.accept(new StatementDeParser(output)); + assertEquals(rule.toString(), output.toString()); + assertTrue(output.toString().contains("DISABLE RULE new_rule")); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + Alter trigger = (Alter) CCJSqlParserUtil.parse("ALTER TABLE t " + state + " TRIGGER trg", + p -> p.withDialect(Dialect.POSTGRESQL)); + RelationAlterAction triggerAction = + (RelationAlterAction) trigger.getAlterExpressions().get(0); + assertEquals(state.replace(' ', '_'), triggerAction.getEnableState().name()); + triggerAction.setEnableState(null); + assertNull(triggerAction.getEnableState()); + assertEquals(RelationAlterAction.Kind.TRIGGER_STATE, triggerAction.getKind()); + assertEquals(RelationAlterAction.TriggerTarget.NAME, triggerAction.getTriggerTarget()); + assertEquals("trg", triggerAction.getValue()); + triggerAction.setEnableState(RelationAlterAction.EnableState.ENABLE_ALWAYS); + assertEquals(RelationAlterAction.EnableState.ENABLE_ALWAYS, triggerAction.getEnableState()); + StringBuilder triggerSql = new StringBuilder(); + trigger.accept(new StatementDeParser(triggerSql)); + assertEquals("ALTER TABLE t ENABLE ALWAYS TRIGGER trg", triggerSql.toString()); + assertEquals(trigger.toString(), triggerSql.toString()); + assertEquals(triggerSql.toString(), CCJSqlParserUtil.parse(triggerSql.toString(), + p -> p.withDialect(Dialect.POSTGRESQL)).toString()); + assertEquals(2, CCJSqlParserUtil.parseStatements(rule + "; SELECT 1").size()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ENABLE RULE", "ALTER TABLE t DISABLE ALWAYS RULE r", + "ALTER TABLE t DISABLE REPLICA RULE r", "ALTER INDEX ix ENABLE RULE r", + "ALTER TABLE t ENABLE ALWAYS TRIGGER ALL", "ALTER TABLE t ENABLE REPLICA TRIGGER USER"}) + void rejectsWrongStateTargets(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL))); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/RenameUserTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/RenameUserTest.java new file mode 100644 index 0000000000..656eb3fd5c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/RenameUserTest.java @@ -0,0 +1,89 @@ +/*- + * #%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.statement.alter; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +class RenameUserTest { + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "MARIADB"}) + void renamesAccounts(Dialect dialect) throws JSQLParserException { + String sql = "RENAME USER 'a'@'%' TO 'b'@'%', old_name TO `new name`@localhost"; + RenameUser renameUser = assertInstanceOf(RenameUser.class, + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(dialect))); + assertEquals(sql, CCJSqlParserUtil + .parse(renameUser.toString(), parser -> parser.withDialect(dialect)).toString()); + + List renames = renameUser.getRenames(); + assertEquals(2, renames.size()); + assertEquals("a", renames.get(0).getFrom().getUser().getValue()); + assertEquals("%", renames.get(0).getTo().getHost().getValue()); + assertEquals("old_name", renames.get(1).getFrom().toString()); + assertEquals("new name", renames.get(1).getTo().getUser().getValue()); + assertEquals("`", renames.get(1).getTo().getUser().getQuoteStr()); + assertEquals("localhost", renames.get(1).getTo().getHost().getValue()); + } + + @Test + void keepsTableRenames() throws JSQLParserException { + assertInstanceOf(RenameTableStatement.class, + TestUtils.assertSqlCanBeParsedAndDeparsed("RENAME TABLE a TO b")); + // a table named user, renamed without the optional TABLE keyword + assertInstanceOf(RenameTableStatement.class, CCJSqlParserUtil.parse("RENAME user TO b")); + } + + @Test + void buildsAndDeparses() { + RenameUser renameUser = new RenameUser().addRename( + new UserAccount().withUser(new StringValue("a")).withHost(new StringValue("%")), + new UserAccount().withUser(new StringValue("b")).withHost(new StringValue("%"))); + + assertDeparse(renameUser, "RENAME USER 'a'@'%' TO 'b'@'%'"); + renameUser.setRenames(null); + assertEquals(0, renameUser.getRenames().size()); + } + + @Test + void reportsSchemaChangeWithoutTables() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("RENAME USER a TO b"); + + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(TablesNamesFinder.findTables(statement.toString())).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"RENAME USER a", "RENAME USER a TO", + "RENAME USER a IDENTIFIED BY 'x' TO b", "RENAME USER a TO b,"}) + void rejectsIncompleteRenames(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/SqlServerDefaultConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/SqlServerDefaultConstraintTest.java new file mode 100644 index 0000000000..fdcc8f5356 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/SqlServerDefaultConstraintTest.java @@ -0,0 +1,165 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.table.DefaultConstraint; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.AlterDeParser; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.validation.validator.AlterValidator; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation; +import net.sf.jsqlparser.util.validation.metadata.Named; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlServerDefaultConstraintTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE dbo.virtual_production_record ADD CONSTRAINT DF_virtual_production_record__d DEFAULT ((0)) FOR _d", + "ALTER TABLE [dbo].[t] ADD CONSTRAINT [DF_value] DEFAULT ((0)) FOR [value] WITH VALUES", + "ALTER TABLE t ADD DEFAULT 0 FOR c", + "ALTER TABLE t ADD DEFAULT NULL FOR c", + "ALTER TABLE t ADD DEFAULT -1 FOR c WITH VALUES", + "ALTER TABLE t ADD DEFAULT N'it''s fine' FOR c", + "ALTER TABLE t ADD CONSTRAINT df DEFAULT (GETDATE()) FOR created_at", + "ALTER TABLE t ADD DEFAULT (NEXT VALUE FOR dbo.seq) FOR id", + "ALTER TABLE t ADD DEFAULT (1 + 2) FOR c", + "ALTER TABLE t ADD CONSTRAINT df DEFAULT CAST(0 AS INT) FOR c", + "ALTER TABLE t ADD DEFAULT 0 FOR a, ADD DEFAULT 1 FOR b", + "ALTER TABLE t ADD DEFAULT 0 FOR c, ADD CONSTRAINT ck CHECK (c >= 0)" + }) + void roundTrip(String sql) throws JSQLParserException { + Alter alter = (Alter) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withSquareBracketQuotation(true)); + assertEquals(alter.toString(), parse(sql).toString()); + assertInstanceOf(DefaultConstraint.class, + alter.getAlterExpressions().get(0).getConstraint()); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t ADD DEFAULT FOR c", + "ALTER TABLE t ADD DEFAULT 0", + "ALTER TABLE t ADD CONSTRAINT df DEFAULT 0 FOR", + "ALTER TABLE t ADD CONSTRAINT DEFAULT 0 FOR c", + "ALTER TABLE t ADD DEFAULT 0 FOR c WITH", + "ALTER TABLE t ADD DEFAULT 0 FOR c WITH VALUES WITH VALUES", + "ALTER TABLE t ALTER CONSTRAINT df DEFAULT 0 FOR c", + "ALTER TABLE t MODIFY DEFAULT 0 FOR c" + }) + void rejectMalformedSyntax(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void inspectAndModifyStructuredConstraint() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT df DEFAULT 0 FOR c WITH VALUES"); + AlterExpression action = alter.getAlterExpressions().get(0); + DefaultConstraint constraint = (DefaultConstraint) action.getConstraint(); + assertEquals(AlterOperation.ADD, action.getOperation()); + assertEquals(ConstraintKind.DEFAULT, constraint.getKind()); + assertEquals("DEFAULT", constraint.getType()); + assertEquals("df", constraint.getName()); + assertEquals("c", constraint.getColumn().getColumnName()); + assertEquals(0, ((LongValue) constraint.getExpression()).getValue()); + assertTrue(constraint.isWithValues()); + constraint.withName((String) null).withExpression(new LongValue(2)) + .withColumn(new Column("other_column")).withWithValues(false); + assertEquals("ALTER TABLE t ADD DEFAULT 2 FOR other_column", alter.toString()); + TestUtils.assertSqlCanBeParsedAndDeparsed(alter.toString()); + } + + @Test + void deparserVisitsDefaultAndTargetColumn() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT df DEFAULT ((0)) FOR c WITH VALUES"); + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append('1'); + } + + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("new_").append(column.getColumnName()); + } + }; + expressions.setBuilder(sql); + new AlterDeParser(sql, expressions).deParse(alter); + assertEquals("ALTER TABLE t ADD CONSTRAINT df DEFAULT ((1)) FOR new_c WITH VALUES", + sql.toString()); + } + + @Test + void validationTraversesStructuredValueAndColumn() throws JSQLParserException { + Alter alter = parse("ALTER TABLE dbo.t ADD DEFAULT (GETDATE()) FOR created_at"); + List visited = new ArrayList<>(); + AlterValidator validator = new AlterValidator() { + @Override + public void validateOptionalExpression(Expression expression) { + visited.add(expression.toString()); + } + }; + validator.setContext(new ValidationContext().setCapabilities(List.of())); + validator.validate(alter); + assertEquals(List.of("(GETDATE())", "created_at"), visited); + assertEquals(Set.of("dbo.t"), new TablesNamesFinder().getTables(alter)); + } + + @Test + void existingColumnDefaultsAndCreateTableRemainSupported() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE t (c INT DEFAULT ((0)), CONSTRAINT pk PRIMARY KEY CLUSTERED (c))"); + TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ALTER COLUMN c SET DEFAULT 0"); + TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ALTER COLUMN c DROP DEFAULT"); + assertEquals(ConstraintKind.DEFAULT, ConstraintKind.fromType("DEFAULT")); + } + + @ParameterizedTest + @ValueSource(strings = {"", "CONSTRAINT df "}) + void validateOptionalConstraintNameAsNewConstraint(String prefix) throws JSQLParserException { + List visited = new ArrayList<>(); + DatabaseMetaDataValidation metadata = named -> { + visited.add(named); + return named.getNamedObject() != NamedObject.constraint; + }; + AlterValidator validator = new AlterValidator(); + validator.setContext(new ValidationContext().setCapabilities(List.of(metadata))); + validator.validate(parse("ALTER TABLE t ADD " + prefix + "DEFAULT 0 FOR c")); + assertTrue(validator.getValidationErrors().isEmpty()); + assertTrue(visited.stream().anyMatch(n -> n.getNamedObject() == NamedObject.column + && n.getFqn().equals("c"))); + assertEquals(prefix.isEmpty() ? 0 : 1, + visited.stream().filter(n -> n.getNamedObject() == NamedObject.constraint).count()); + assertTrue(visited.stream().noneMatch(n -> n.getNamedObject() == NamedObject.index)); + } + + private Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.SQLSERVER).withSquareBracketQuotation(true) + .withUnsupportedStatements(false)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/database/AlterDatabaseTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/database/AlterDatabaseTest.java new file mode 100644 index 0000000000..48a10da952 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/database/AlterDatabaseTest.java @@ -0,0 +1,256 @@ +/*- + * #%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.statement.alter.database; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.statement.create.database.DatabaseOption; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterDatabaseTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER DATABASE d CHARACTER SET utf8mb4 COLLATE utf8mb4_bin", + "ALTER SCHEMA `d` DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_bin", + "ALTER DATABASE d DEFAULT ENCRYPTION = 'N'", + "ALTER DATABASE d READ ONLY = 0", "ALTER DATABASE d READ ONLY 1", + "ALTER DATABASE READ ONLY = DEFAULT", "ALTER SCHEMA DEFAULT CHARACTER SET utf8mb4"}) + void roundTripAndStatementBoundary(String sql) throws JSQLParserException { + AlterDatabase statement = parse(sql); + assertEquals(sql, statement.toString()); + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(sql, buffer.toString()); + assertEquals(sql, parse(buffer.toString()).toString()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.MYSQL)).size()); + } + + @Test + void optionMutationAndVisitorDispatch() throws JSQLParserException { + AlterDatabase statement = parse("ALTER DATABASE old_db READ ONLY = 0"); + statement.setDatabaseName("new_db"); + statement.getOption(DatabaseOption.Kind.READ_ONLY).orElseThrow().setValue("1"); + assertEquals("ALTER DATABASE new_db READ ONLY = 1", statement.toString()); + assertEquals("context:new_db", statement.accept(new StatementVisitorAdapter() { + @Override + public String visit(AlterDatabase database, S context) { + return context + ":" + database.getDatabaseName(); + } + }, "context")); + } + + @Test + void rejectIncompleteAndInvalidReadOnly() { + for (String sql : new String[] {"ALTER DATABASE d", "ALTER DATABASE d READ ONLY = 2", + "ALTER DATABASE d DEFAULT READ ONLY = 0"}) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER DATABASE db1 DEFAULT CHARSET = utf8mb4", + "ALTER SCHEMA db1 DEFAULT ENCRYPTION = 'Y'", "ALTER SCHEMA db1 CHAR SET utf8mb4", + "ALTER DATABASE CHARSET utf8mb4 COLLATE utf8mb4_bin", + "ALTER DATABASE charset CHARSET = utf8mb4", + "ALTER DATABASE READ ONLY = DEFAULT", "ALTER SCHEMA DEFAULT CHARACTER SET utf8mb4"}) + void testMySqlOptionsWithAndWithoutDialect(String sql) throws JSQLParserException { + assertRoundTrip(sql, Dialect.MYSQL); + assertRoundTrip(sql, null); + } + + @Test + void testCharsetSpellingAndOmittedName() throws JSQLParserException { + AlterDatabase statement = + (AlterDatabase) CCJSqlParserUtil.parse("ALTER SCHEMA DEFAULT CHARSET utf8mb4"); + assertTrue(statement.isUseSchemaKeyword()); + assertNull(statement.getDatabaseName()); + DatabaseOption charset = + statement.getOption(DatabaseOption.Kind.CHARACTER_SET).orElseThrow(); + assertEquals("CHARSET", charset.getName()); + assertEquals("utf8mb4", charset.getValue()); + assertTrue(charset.isUseDefault()); + + // CHARSET is also a valid database name. + statement = + (AlterDatabase) CCJSqlParserUtil.parse("ALTER DATABASE charset CHARSET utf8mb4"); + assertEquals("charset", statement.getDatabaseName()); + statement = (AlterDatabase) CCJSqlParserUtil.parse("ALTER DATABASE charset RENAME TO c2", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals("charset", statement.getDatabaseName()); + assertEquals("c2", statement.getNewName()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER DATABASE mydb RENAME TO mydb2", + "ALTER DATABASE \"My Db\" RENAME TO \"Other Db\"", "ALTER DATABASE mydb OWNER TO u1", + "ALTER DATABASE mydb OWNER TO CURRENT_USER", + "ALTER DATABASE mydb OWNER TO CURRENT_ROLE", + "ALTER DATABASE mydb OWNER TO SESSION_USER", "ALTER DATABASE mydb SET TABLESPACE fast", + "ALTER DATABASE mydb REFRESH COLLATION VERSION", + "ALTER DATABASE mydb SET search_path TO app, public", + "ALTER DATABASE mydb SET search_path = \"$user\", public", + "ALTER DATABASE mydb SET work_mem = '64MB'", + "ALTER DATABASE mydb SET work_mem TO DEFAULT", + "ALTER DATABASE mydb SET work_mem FROM CURRENT", "ALTER DATABASE mydb RESET work_mem", + "ALTER DATABASE mydb RESET ALL", "ALTER DATABASE mydb WITH ALLOW_CONNECTIONS false", + "ALTER DATABASE mydb CONNECTION LIMIT = -1 IS_TEMPLATE true", + "ALTER DATABASE mydb WITH IS_TEMPLATE = true ALLOW_CONNECTIONS = false CONNECTION LIMIT 5"}) + void testPostgreSqlForms(String sql) throws JSQLParserException { + assertRoundTrip(sql, Dialect.POSTGRESQL); + assertRoundTrip(sql, null); + } + + @Test + void testPostgreSqlActions() throws JSQLParserException { + AlterDatabase rename = parsePostgreSql("ALTER DATABASE mydb RENAME TO mydb2"); + assertEquals(AlterDatabase.Action.RENAME, rename.getAction()); + assertEquals("mydb", rename.getDatabaseName()); + assertEquals("mydb2", rename.getNewName()); + assertTrue(rename.getOptions().isEmpty()); + + AlterDatabase owner = parsePostgreSql("ALTER DATABASE mydb OWNER TO CURRENT_USER"); + assertEquals(AlterDatabase.Action.OWNER, owner.getAction()); + assertEquals("CURRENT_USER", owner.getOwner()); + + AlterDatabase tablespace = parsePostgreSql("ALTER DATABASE mydb SET TABLESPACE fast"); + assertEquals(AlterDatabase.Action.SET_TABLESPACE, tablespace.getAction()); + assertEquals("fast", tablespace.getTablespace()); + assertNull(tablespace.getParameterChange()); + + assertEquals(AlterDatabase.Action.REFRESH_COLLATION_VERSION, + parsePostgreSql("ALTER DATABASE mydb REFRESH COLLATION VERSION").getAction()); + + // A configuration parameter named "tablespace" is a SET, not SET TABLESPACE. + AlterDatabase parameter = parsePostgreSql("ALTER DATABASE mydb SET tablespace = fast"); + assertEquals(AlterDatabase.Action.CONFIGURATION_PARAMETER, parameter.getAction()); + assertEquals("tablespace", parameter.getParameterChange().getParameter()); + } + + @Test + void testPostgreSqlOptions() throws JSQLParserException { + AlterDatabase statement = parsePostgreSql( + "ALTER DATABASE mydb WITH ALLOW_CONNECTIONS false CONNECTION LIMIT = -1 IS_TEMPLATE true"); + assertEquals(AlterDatabase.Action.OPTIONS, statement.getAction()); + assertTrue(statement.isUseWith()); + List options = statement.getOptions(); + assertEquals(3, options.size()); + assertOption(options.get(0), DatabaseOption.Kind.ALLOW_CONNECTIONS, "ALLOW_CONNECTIONS", + "false", false); + assertOption(options.get(1), DatabaseOption.Kind.CONNECTION_LIMIT, "CONNECTION LIMIT", + "-1", true); + assertOption(options.get(2), DatabaseOption.Kind.IS_TEMPLATE, "IS_TEMPLATE", "true", + false); + + statement.getOption(DatabaseOption.Kind.CONNECTION_LIMIT).orElseThrow().setValue("10"); + statement.setUseWith(false); + assertEquals("ALTER DATABASE mydb ALLOW_CONNECTIONS false CONNECTION LIMIT = 10 " + + "IS_TEMPLATE true", statement.toString()); + } + + @Test + void testSwitchingActionsKeepsPayloads() throws JSQLParserException { + AlterDatabase statement = parsePostgreSql("ALTER DATABASE mydb RENAME TO mydb2"); + statement.setAction(AlterDatabase.Action.OWNER); + statement.setOwner("SESSION_USER"); + assertEquals("ALTER DATABASE mydb OWNER TO SESSION_USER", statement.toString()); + statement.setAction(AlterDatabase.Action.RENAME); + assertEquals("ALTER DATABASE mydb RENAME TO mydb2", statement.toString()); + + statement.setAction(AlterDatabase.Action.CONFIGURATION_PARAMETER); + assertThrows(IllegalStateException.class, statement::toString); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER DATABASE d", "ALTER DATABASE d SET TIME ZONE 'UTC'", + "ALTER DATABASE d SET RECOVERY SIMPLE", "ALTER DATABASE d MODIFY NAME = d2", + "ALTER DATABASE WITH ALLOW_CONNECTIONS true"}) + void testUnmodelledFormsRemainUnsupportedOutsideMySql(String sql) + throws JSQLParserException { + assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql)); + assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void testPostgreSqlAlterSchemaIsUnchanged() throws JSQLParserException { + assertInstanceOf(AlterSchema.class, CCJSqlParserUtil.parse("ALTER SCHEMA s RENAME TO t")); + assertInstanceOf(AlterSchema.class, + CCJSqlParserUtil.parse("ALTER SCHEMA s OWNER TO CURRENT_USER")); + assertInstanceOf(AlterDatabase.class, + CCJSqlParserUtil.parse("ALTER SCHEMA s DEFAULT ENCRYPTION = 'Y'")); + } + + @Test + void testValidation() { + validateNoErrors("ALTER DATABASE mydb SET work_mem = '64MB'", 1, DatabaseType.POSTGRESQL); + validateNoErrors("ALTER DATABASE mydb OWNER TO CURRENT_USER", 1, DatabaseType.POSTGRESQL); + validateNoErrors("ALTER DATABASE d CHARACTER SET utf8mb4", 1, DatabaseType.MYSQL, + DatabaseType.MARIADB); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), + "ALTER DATABASE mydb RESET ALL").validate().isEmpty()); + assertTrue(new Validation(List.of(new FeaturesAllowed().add(Feature.alterDatabase)), + "ALTER DATABASE mydb RESET ALL").validate().isEmpty()); + } + + private static void assertOption(DatabaseOption option, DatabaseOption.Kind kind, String name, + String value, boolean useEquals) { + assertEquals(kind, option.getKind()); + assertEquals(name, option.getName()); + assertEquals(value, option.getValue()); + assertEquals(useEquals, option.isUseEquals()); + assertFalse(option.isUseDefault()); + } + + /** Parses, deparses and reparses with the dialect, or without one if it is null. */ + private static void assertRoundTrip(String sql, Dialect dialect) throws JSQLParserException { + Consumer consumer = + dialect == null ? parser -> { + } : parser -> parser.withDialect(dialect); + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, consumer); + assertInstanceOf(AlterDatabase.class, statement); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", consumer).size()); + } + + private static AlterDatabase parsePostgreSql(String sql) throws JSQLParserException { + return (AlterDatabase) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static AlterDatabase parse(String sql) throws JSQLParserException { + return (AlterDatabase) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/analyze/AnalyzeTest.java b/src/test/java/net/sf/jsqlparser/statement/analyze/AnalyzeTest.java index 7f19370f45..ea00590eae 100644 --- a/src/test/java/net/sf/jsqlparser/statement/analyze/AnalyzeTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/analyze/AnalyzeTest.java @@ -9,16 +9,25 @@ */ package net.sf.jsqlparser.statement.analyze; +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.StringReader; +import java.util.List; +import java.util.stream.Collectors; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserManager; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import org.junit.jupiter.api.Test; - -import java.io.StringReader; - -import static net.sf.jsqlparser.test.TestUtils.assertDeparse; -import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class AnalyzeTest { @@ -38,4 +47,93 @@ public void testAnalyze() throws JSQLParserException { public void testAnalyze2() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("ANALYZE mytable"); } + + @Test + public void testMySqlAnalyzeTableList() throws JSQLParserException { + String sql = "ANALYZE TABLE t2, t3"; + Analyze analyze = (Analyze) parserManager.parse(new StringReader(sql)); + + assertEquals(List.of("t2", "t3"), analyze.getTables().stream() + .map(Table::getFullyQualifiedName).collect(Collectors.toList())); + assertEquals("t2", analyze.getTable().getFullyQualifiedName()); + assertEquals(sql, analyze.toString()); + + assertDeparse(new Analyze().withTableKeyword(true) + .withTables(List.of(new Table("t2"), new Table("t3"))), sql); + } + + @Test + public void testMySqlAnalyzeUpdateHistogram() throws JSQLParserException { + String sql = "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1, c2 WITH 64 BUCKETS"; + Analyze analyze = (Analyze) assertSqlCanBeParsedAndDeparsed(sql); + + assertEquals(Analyze.HistogramAction.UPDATE, analyze.getHistogramAction()); + assertEquals(List.of("c1", "c2"), analyze.getHistogramColumns().stream() + .map(Column::getFullyQualifiedName).collect(Collectors.toList())); + assertEquals(64, analyze.getHistogramBucketCount()); + } + + @Test + public void testMySqlAnalyzeDropHistogram() throws JSQLParserException { + String sql = "ANALYZE LOCAL TABLE t2 DROP HISTOGRAM ON c1"; + Analyze analyze = (Analyze) assertSqlCanBeParsedAndDeparsed(sql); + + assertEquals(Analyze.Modifier.LOCAL, analyze.getModifier()); + assertEquals(Analyze.HistogramAction.DROP, analyze.getHistogramAction()); + assertEquals("c1", analyze.getHistogramColumns().get(0).getFullyQualifiedName()); + } + + @Test + public void testMySqlAnalyzeNoWriteToBinlog() throws JSQLParserException { + String sql = "ANALYZE NO_WRITE_TO_BINLOG TABLE t1"; + Analyze analyze = (Analyze) assertSqlCanBeParsedAndDeparsed(sql); + + assertEquals(Analyze.Modifier.NO_WRITE_TO_BINLOG, analyze.getModifier()); + } + + @Test + public void testMySqlAnalyzeHistogramUpdateMode() throws JSQLParserException { + Analyze auto = (Analyze) assertSqlCanBeParsedAndDeparsed( + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1, c2 WITH 32 BUCKETS AUTO UPDATE"); + assertEquals(32, auto.getHistogramBucketCount()); + assertEquals(Analyze.HistogramUpdateMode.AUTO, auto.getHistogramUpdateMode()); + + Analyze manual = (Analyze) assertSqlCanBeParsedAndDeparsed( + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 MANUAL UPDATE"); + assertNull(manual.getHistogramBucketCount()); + assertEquals(Analyze.HistogramUpdateMode.MANUAL, manual.getHistogramUpdateMode()); + + assertDeparse(new Analyze().withTableKeyword(true).withTable(new Table("t1")) + .withHistogramAction(Analyze.HistogramAction.UPDATE) + .addHistogramColumns(new Column("c1")) + .withHistogramUpdateMode(Analyze.HistogramUpdateMode.AUTO), + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 AUTO UPDATE"); + } + + @Test + public void testMySqlAnalyzeHistogramUsingData() throws JSQLParserException { + String json = "{\"buckets\": [[1, 0.5], [2, 1.0]], \"data-type\": \"int\"}"; + String sql = "ANALYZE NO_WRITE_TO_BINLOG TABLE t1 UPDATE HISTOGRAM ON c1 USING DATA '" + + json + "'"; + Analyze analyze = (Analyze) assertSqlCanBeParsedAndDeparsed(sql); + assertEquals(json, analyze.getHistogramData().getValue()); + assertNull(analyze.getHistogramUpdateMode()); + assertEquals(sql, CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)) + .toString()); + + analyze.setHistogramData(new StringValue("{}")); + assertEquals("ANALYZE NO_WRITE_TO_BINLOG TABLE t1 UPDATE HISTOGRAM ON c1 USING DATA '{}'", + analyze.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"ANALYZE TABLE t1 DROP HISTOGRAM ON c1 AUTO UPDATE", + "ANALYZE TABLE t1 DROP HISTOGRAM ON c1 USING DATA '{}'", + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 USING DATA '{}' MANUAL UPDATE", + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 MANUAL UPDATE WITH 8 BUCKETS", + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 AUTO", + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 USING DATA"}) + public void testMySqlAnalyzeRejectsMisplacedHistogramOptions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java b/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java index 3fbfe8d962..9d9b457613 100644 --- a/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java @@ -141,6 +141,7 @@ public class ReflectionModelTest { new net.sf.jsqlparser.statement.alter.AlterExpression(), new net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType(false), new net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDropNotNull("name"), + new net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull("name"), new net.sf.jsqlparser.statement.merge.MergeInsert(), new net.sf.jsqlparser.statement.alter.DeferrableConstraint(), new net.sf.jsqlparser.statement.alter.EnableConstraint(), @@ -157,10 +158,10 @@ public class ReflectionModelTest { new net.sf.jsqlparser.statement.create.table.ColumnDefinition(), new net.sf.jsqlparser.statement.create.table.CreateTable(), new net.sf.jsqlparser.statement.create.table.ExcludeConstraint(), - new net.sf.jsqlparser.statement.create.table.ForeignKeyIndex(), + new net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint(), new net.sf.jsqlparser.statement.create.table.Index(), - new net.sf.jsqlparser.statement.create.table.Index.ColumnParams("p"), - new net.sf.jsqlparser.statement.create.table.NamedConstraint(), + new net.sf.jsqlparser.statement.create.table.KeyElement("p"), + new net.sf.jsqlparser.statement.create.table.KeyConstraint(), new net.sf.jsqlparser.statement.create.table.RowMovement(), new net.sf.jsqlparser.statement.create.view.AlterView(), new net.sf.jsqlparser.statement.create.view.CreateView(), diff --git a/src/test/java/net/sf/jsqlparser/statement/comment/CommentTargetTest.java b/src/test/java/net/sf/jsqlparser/statement/comment/CommentTargetTest.java new file mode 100644 index 0000000000..e48148fac7 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/comment/CommentTargetTest.java @@ -0,0 +1,683 @@ +/*- + * #%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.statement.comment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ExclusionOperator; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class CommentTargetTest { + // PostgreSQL accepts schema-qualified names only for these kinds + private static final Set SCHEMA_QUALIFIED = EnumSet.of( + CommentTarget.Kind.INDEX, CommentTarget.Kind.SEQUENCE, CommentTarget.Kind.DOMAIN, + CommentTarget.Kind.TYPE, CommentTarget.Kind.MATERIALIZED_VIEW, + CommentTarget.Kind.FOREIGN_TABLE, CommentTarget.Kind.COLLATION, + CommentTarget.Kind.STATISTICS, CommentTarget.Kind.CONVERSION, + CommentTarget.Kind.TEXT_SEARCH_CONFIGURATION, CommentTarget.Kind.TEXT_SEARCH_DICTIONARY, + CommentTarget.Kind.TEXT_SEARCH_PARSER, CommentTarget.Kind.TEXT_SEARCH_TEMPLATE); + + private static final Set UNQUALIFIED = EnumSet.of( + CommentTarget.Kind.SCHEMA, CommentTarget.Kind.ACCESS_METHOD, + CommentTarget.Kind.DATABASE, CommentTarget.Kind.EVENT_TRIGGER, + CommentTarget.Kind.EXTENSION, CommentTarget.Kind.FOREIGN_DATA_WRAPPER, + CommentTarget.Kind.LANGUAGE, CommentTarget.Kind.PUBLICATION, CommentTarget.Kind.ROLE, + CommentTarget.Kind.SERVER, CommentTarget.Kind.SUBSCRIPTION, + CommentTarget.Kind.TABLESPACE); + + static Stream names() { + List cases = new ArrayList<>(); + Set kinds = EnumSet.copyOf(SCHEMA_QUALIFIED); + kinds.addAll(UNQUALIFIED); + for (CommentTarget.Kind kind : kinds) { + for (String name : List.of("obj", "\"a.b\"", "\"a\"\"b.c\"", "\"한.글\"", "\".\"")) { + cases.add(Arguments.of(kind, name, null, name)); + } + if (SCHEMA_QUALIFIED.contains(kind)) { + cases.add(Arguments.of(kind, "public.\"a.b\"", "public", "\"a.b\"")); + cases.add(Arguments.of(kind, "\"s.p\".\"a.b\"", "\"s.p\"", "\"a.b\"")); + } + } + return cases.stream(); + } + + @ParameterizedTest + @MethodSource("names") + void preservesNamedTargetsAndTheirComponents(CommentTarget.Kind kind, String name, + String schema, String leaf) throws Exception { + for (String literal : List.of("'body'", "$tag$body;\nquote's \\path$tag$", "NULL", "''")) { + String sql = + "COMMENT ON " + kind.name().replace('_', ' ') + " " + name + " IS " + literal; + for (Comment comment : roundTrip(sql)) { + assertNull(comment.getTable()); + assertNull(comment.getColumn()); + assertNull(comment.getView()); + CommentTarget target = comment.getTarget(); + assertEquals(kind, target.getKind()); + assertEquals(leaf, target.getName().getName()); + assertEquals(schema, target.getName().getSchemaName()); + assertEquals(name, target.getName().getFullyQualifiedName()); + if (literal.equals("NULL")) { + assertNull(comment.getComment()); + } else { + assertEquals(literal, comment.getComment().toString()); + } + } + } + } + + static Stream signatures() { + List cases = new ArrayList<>(); + for (CommentTarget.Kind kind : List.of(CommentTarget.Kind.FUNCTION, + CommentTarget.Kind.PROCEDURE, CommentTarget.Kind.ROUTINE)) { + for (String signature : List.of("f", "f()", "f(integer)", "f(IN arg integer)", + "f(OUT result text)", "f(INOUT arg integer)", "f(VARIADIC args text[])", + "\"s.p\".\"f.x\"(IN \"arg.x\" pg_catalog.int4[])", + "f(timestamp with time zone, double precision)")) { + cases.add(Arguments.of(kind, signature)); + } + } + return cases.stream(); + } + + @ParameterizedTest + @MethodSource("signatures") + void preservesRoutineSignaturesWithoutTreatingThemAsCalls(CommentTarget.Kind kind, + String signature) throws Exception { + for (Comment comment : roundTrip("COMMENT ON " + kind + " " + signature + " IS 'body'")) { + assertEquals(kind, comment.getTarget().getKind()); + assertNull(comment.getTarget().getName()); + RoutineReference routine = comment.getTarget().getRoutine(); + assertEquals( + signature.contains("(") ? signature.substring(0, signature.indexOf('(')) + : signature, + routine.getName()); + assertEquals(!signature.contains("("), routine.getArguments() == null); + assertTrue(new TablesNamesFinder().getTables(comment).isEmpty()); + assertTrue(comment.getFeatures().modifiesSchema()); + assertFalse(comment.getFeatures().may(StmtFeature.READS_DATA)); + assertFalse(comment.getFeatures().isOpaque()); + assertTrue(comment.getFeatures().getUnresolvedReferences().isEmpty()); + } + } + + @Test + void exposesArgumentModeNameAndDataType() throws Exception { + Comment comment = parse( + "COMMENT ON FUNCTION f(INOUT \"arg.x\" app.custom_type, VARIADIC rest text[]) IS NULL"); + List arguments = comment.getTarget().getRoutine().getArguments(); + assertEquals(RoutineReference.Argument.Mode.INOUT, arguments.get(0).getMode()); + assertEquals("\"arg.x\"", arguments.get(0).getName()); + assertEquals("app.custom_type", arguments.get(0).getDataType().toString()); + assertEquals(RoutineReference.Argument.Mode.VARIADIC, arguments.get(1).getMode()); + assertEquals("rest", arguments.get(1).getName()); + assertEquals("text[]", arguments.get(1).getDataType().toString()); + } + + @Test + void preservesAggregateSignatures() throws Exception { + RoutineReference star = aggregate("app.agg(*)"); + assertEquals("app.agg", star.getName()); + assertTrue(star.isAllArguments()); + assertNull(star.getOrderByArguments()); + + RoutineReference direct = aggregate("agg(integer, text)"); + assertFalse(direct.isAllArguments()); + assertEquals(2, direct.getArguments().size()); + assertNull(direct.getOrderByArguments()); + + RoutineReference ordered = aggregate("rank(VARIADIC \"any\" ORDER BY VARIADIC \"any\")"); + assertEquals(RoutineReference.Argument.Mode.VARIADIC, + ordered.getArguments().get(0).getMode()); + assertEquals(RoutineReference.Argument.Mode.VARIADIC, + ordered.getOrderByArguments().get(0).getMode()); + + // ORDER is not reserved, so it must not be taken for the name of a direct argument + RoutineReference orderedOnly = aggregate("mode(ORDER BY anyelement)"); + assertTrue(orderedOnly.getArguments().isEmpty()); + assertEquals("anyelement", + orderedOnly.getOrderByArguments().get(0).getDataType().toString()); + } + + private static RoutineReference aggregate(String signature) throws JSQLParserException { + List comments = roundTrip("COMMENT ON AGGREGATE " + signature + " IS NULL"); + for (Comment comment : comments) { + assertEquals(CommentTarget.Kind.AGGREGATE, comment.getTarget().getKind()); + assertEquals(signature, comment.getTarget().getRoutine().toString()); + assertTrue(new TablesNamesFinder().getTables(comment).isEmpty()); + } + return comments.get(0).getTarget().getRoutine(); + } + + @ParameterizedTest + @ValueSource(strings = {"t", "\"t.x\"", "public.\"t.x\"", "\"s.p\".\"t.x\""}) + void distinguishesConstraintTablesFromDomains(String owner) throws Exception { + for (boolean domain : List.of(false, true)) { + String sql = "COMMENT ON CONSTRAINT \"c.k\" ON " + (domain ? "DOMAIN " : "") + owner + + " IS $$body$$"; + for (Comment comment : roundTrip(sql)) { + CommentTarget target = comment.getTarget(); + assertEquals(CommentTarget.Kind.CONSTRAINT, target.getKind()); + assertEquals("\"c.k\"", target.getName().getName()); + assertNull(target.getName().getSchemaName()); + assertEquals(domain, target.isOnDomain()); + assertEquals(owner, target.getRelation().getFullyQualifiedName()); + assertEquals(domain ? Set.of() : Set.of(owner), + new TablesNamesFinder().getTables(comment)); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"domain", "domain.t", "\"DOMAIN\""}) + void domainKeywordCanNameTheConstraintTable(String name) throws Exception { + for (Comment comment : roundTrip("COMMENT ON CONSTRAINT ck ON " + name + " IS NULL")) { + assertFalse(comment.getTarget().isOnDomain()); + assertEquals(name, comment.getTarget().getRelation().getFullyQualifiedName()); + assertEquals(Set.of(name), new TablesNamesFinder().getTables(comment)); + } + } + + static Stream relationOwnedTargets() { + List cases = new ArrayList<>(); + for (CommentTarget.Kind kind : List.of(CommentTarget.Kind.TRIGGER, + CommentTarget.Kind.POLICY, CommentTarget.Kind.RULE)) { + for (String owner : List.of("t", "\"t.x\"", "public.\"t.x\"", "\"s.p\".\"t.x\"", + "domain")) { + cases.add(Arguments.of(kind, owner)); + } + } + return cases.stream(); + } + + @ParameterizedTest + @MethodSource("relationOwnedTargets") + void namesTriggersPoliciesAndRulesWithinTheirTable(CommentTarget.Kind kind, String owner) + throws Exception { + for (Comment comment : roundTrip( + "COMMENT ON " + kind + " \"o.n\" ON " + owner + " IS 'body'")) { + CommentTarget target = comment.getTarget(); + assertEquals(kind, target.getKind()); + assertEquals("\"o.n\"", target.getName().getName()); + assertNull(target.getName().getSchemaName()); + assertFalse(target.isOnDomain()); + assertEquals(owner, target.getRelation().getFullyQualifiedName()); + assertEquals(Set.of(owner), new TablesNamesFinder().getTables(comment)); + } + } + + @ParameterizedTest + @ValueSource(strings = {"bigint AS int4", "character varying AS text", + "text[] AS app.custom_type", "timestamp with time zone AS date", + "double precision AS time without time zone"}) + void preservesCastSourceAndTargetTypes(String types) throws Exception { + for (Comment comment : roundTrip("COMMENT ON CAST (" + types + ") IS 'body'")) { + CommentTarget target = comment.getTarget(); + assertEquals(CommentTarget.Kind.CAST, target.getKind()); + assertNull(target.getName()); + assertNull(target.getRoutine()); + assertEquals(types, target.getSourceType() + " AS " + target.getTargetType()); + assertTrue(new TablesNamesFinder().getTables(comment).isEmpty()); + } + } + + @Test + void recordsProceduralLanguageSpelling() throws Exception { + for (boolean procedural : List.of(false, true)) { + String sql = "COMMENT ON " + (procedural ? "PROCEDURAL " : "") + + "LANGUAGE plpgsql IS 'body'"; + for (Comment comment : roundTrip(sql)) { + assertEquals(CommentTarget.Kind.LANGUAGE, comment.getTarget().getKind()); + assertEquals(procedural, comment.getTarget().isProcedural()); + assertEquals("plpgsql", comment.getTarget().getName().getName()); + assertEquals(sql, comment.toString()); + } + } + } + + static Stream operators() { + List cases = new ArrayList<>(); + // arithmetic, comparison, bitwise, string, pattern, JSON, array, range and geometric + for (String operator : List.of("+", "-", "*", "/", "%", "^", "|/", "||/", "@", "<", ">", + "=", "<=", ">=", "<>", "!=", "&", "|", "#", "~", "<<", ">>", "||", "~~", "~~*", + "!~~", "!~~*", "~*", "!~", "!~*", "^@", "->", "->>", "#>", "#>>", "#-", "@>", "<@", + "?", "?|", "?&", "@?", "@@", "@@@", "&&", "<->", "<#>", "<=>", "-|-", "<<=", ">>=", + "&<", "&>", "<<|", "|>>", "&<|", "|&>", "<^", ">^", "?#", "?-|", "?||", "~=", + "@-@", "##", "!!", "*=", "*<>", "~>=~")) { + cases.add(Arguments.of(operator, null, operator)); + } + cases.add(Arguments.of("app.+", "app", "+")); + cases.add(Arguments.of("pg_catalog.||", "pg_catalog", "||")); + cases.add(Arguments.of("\"My.Schema\".->>", "\"My.Schema\"", "->>")); + // CLASS and FAMILY only start an operator class or family when no dot follows + cases.add(Arguments.of("class.<<=", "class", "<<=")); + cases.add(Arguments.of("family.-|-", "family", "-|-")); + return cases.stream(); + } + + @ParameterizedTest + @MethodSource("operators") + void preservesOperatorNamesAndOperandTypes(String operator, String schema, String symbol) + throws Exception { + for (Comment comment : roundTrip( + "COMMENT ON OPERATOR " + operator + " (integer, app.t[]) IS 'body'")) { + CommentTarget target = comment.getTarget(); + assertEquals(CommentTarget.Kind.OPERATOR, target.getKind()); + assertNull(target.getName()); + assertEquals(schema, target.getOperator().getSchemaName()); + assertEquals(symbol, target.getOperator().getName()); + assertFalse(target.getOperator().isUseOperatorKeyword()); + assertEquals("integer", target.getLeftOperandType().toString()); + assertEquals("app.t[]", target.getRightOperandType().toString()); + assertTrue(new TablesNamesFinder().getTables(comment).isEmpty()); + } + } + + @Test + void joinsOnlyOperatorPartsThatTouch() throws Exception { + assertEquals("COMMENT ON OPERATOR <<= (inet, inet) IS NULL", + parse("COMMENT ON OPERATOR <<=(inet,inet) IS NULL").toString()); + assertEquals("COMMENT ON OPERATOR - (NONE, integer) IS NULL", + parse("COMMENT ON OPERATOR -/* prefix */(NONE, integer) IS NULL").toString()); + for (String operator : List.of("< =", "< >", "> =", "! =", "+ -", "+/**/-", "-\n|-")) { + assertThrows(JSQLParserException.class, + () -> parse("COMMENT ON OPERATOR " + operator + " (integer, integer) IS NULL"), + operator); + } + } + + @ParameterizedTest + @ValueSource(strings = {"NONE, integer", "integer, NONE", "\"NONE\", none", + "timestamp with time zone, double precision", "NONE, character varying"}) + void representsMissingOperandsAsNull(String operands) throws Exception { + String[] types = operands.split(", "); + for (Comment comment : roundTrip("COMMENT ON OPERATOR - (" + operands + ") IS NULL")) { + CommentTarget target = comment.getTarget(); + assertEquals(types[0].equals("NONE") ? null : types[0], + target.getLeftOperandType() == null ? null + : target.getLeftOperandType().toString()); + assertEquals(types[1].equalsIgnoreCase("NONE") ? null : types[1], + target.getRightOperandType() == null ? null + : target.getRightOperandType().toString()); + assertEquals("OPERATOR - (" + operands.replace("none", "NONE") + ")", + target.toString()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"int4_ops USING btree", "app.\"my.ops\" USING gist", + "\"s.p\".ops USING \"my am\"", "class USING family"}) + void namesOperatorClassesAndFamiliesWithinTheirAccessMethod(String reference) + throws Exception { + String name = reference.substring(0, reference.indexOf(" USING ")); + String method = reference.substring(reference.indexOf(" USING ") + 7); + for (CommentTarget.Kind kind : List.of(CommentTarget.Kind.OPERATOR_CLASS, + CommentTarget.Kind.OPERATOR_FAMILY)) { + String sql = "COMMENT ON " + kind.name().replace('_', ' ') + " " + reference + + " IS 'body'"; + for (Comment comment : roundTrip(sql)) { + CommentTarget target = comment.getTarget(); + assertEquals(kind, target.getKind()); + assertEquals(name, target.getName().getFullyQualifiedName()); + assertEquals(method, target.getAccessMethod()); + assertNull(target.getRelation()); + assertNull(target.getOperator()); + assertEquals(sql, comment.toString()); + assertTrue(new TablesNamesFinder().getTables(comment).isEmpty()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"0", "4242", "0004242", "4294967295"}) + void storesLargeObjectOids(String oid) throws Exception { + for (Comment comment : roundTrip("COMMENT ON LARGE OBJECT " + oid + " IS 'lo'")) { + CommentTarget target = comment.getTarget(); + assertEquals(CommentTarget.Kind.LARGE_OBJECT, target.getKind()); + assertEquals(Long.valueOf(oid), target.getLargeObjectOid()); + assertNull(target.getName()); + assertEquals("COMMENT ON LARGE OBJECT " + Long.valueOf(oid) + " IS 'lo'", + comment.toString()); + assertTrue(new TablesNamesFinder().getTables(comment).isEmpty()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"int LANGUAGE sql", "hstore LANGUAGE plpython3u", + "app.\"my.type\"[] LANGUAGE \"my lang\"", + "timestamp with time zone LANGUAGE plperl", "language LANGUAGE language"}) + void preservesTransformTypeAndLanguage(String reference) throws Exception { + String type = reference.substring(0, reference.indexOf(" LANGUAGE ")); + String language = reference.substring(reference.indexOf(" LANGUAGE ") + 10); + for (Comment comment : roundTrip("COMMENT ON TRANSFORM FOR " + reference + " IS NULL")) { + CommentTarget target = comment.getTarget(); + assertEquals(CommentTarget.Kind.TRANSFORM, target.getKind()); + assertEquals(type, target.getTransformType().toString()); + assertEquals(language, target.getLanguage()); + assertNull(target.getName()); + assertNull(target.getSourceType()); + assertTrue(new TablesNamesFinder().getTables(comment).isEmpty()); + } + } + + @Test + void kindPredicatesDescribeTheReferenceShape() { + Set routines = EnumSet.noneOf(CommentTarget.Kind.class); + Set owned = EnumSet.noneOf(CommentTarget.Kind.class); + Set accessMethod = EnumSet.noneOf(CommentTarget.Kind.class); + for (CommentTarget.Kind kind : CommentTarget.Kind.values()) { + if (kind.isRoutine()) { + routines.add(kind); + } + if (kind.isOwnedByRelation()) { + owned.add(kind); + } + if (kind.isOwnedByAccessMethod()) { + accessMethod.add(kind); + } + } + assertEquals(EnumSet.of(CommentTarget.Kind.FUNCTION, CommentTarget.Kind.PROCEDURE, + CommentTarget.Kind.ROUTINE, CommentTarget.Kind.AGGREGATE), routines); + assertEquals(EnumSet.of(CommentTarget.Kind.CONSTRAINT, CommentTarget.Kind.TRIGGER, + CommentTarget.Kind.POLICY, CommentTarget.Kind.RULE), owned); + assertEquals(EnumSet.of(CommentTarget.Kind.OPERATOR_CLASS, + CommentTarget.Kind.OPERATOR_FAMILY), accessMethod); + } + + @ParameterizedTest + @ValueSource(strings = {"COMMENT ON TRIGGER trg ON users IS 'x'", + "COMMENT ON POLICY p1 ON users IS 'x'", "COMMENT ON RULE r1 ON app.v IS NULL", + "COMMENT ON EXTENSION postgis IS 'x'", "COMMENT ON DATABASE mydb IS 'x'", + "COMMENT ON ROLE app_read IS 'x'", "COMMENT ON TABLESPACE pg_default IS ''", + "COMMENT ON EVENT TRIGGER et IS 'x'", "COMMENT ON PUBLICATION pub IS 'x'", + "COMMENT ON SUBSCRIPTION sub IS 'x'", "COMMENT ON SERVER srv IS 'x'", + "COMMENT ON FOREIGN DATA WRAPPER postgres_fdw IS 'x'", + "COMMENT ON FOREIGN TABLE app.ft IS 'x'", + "COMMENT ON PROCEDURAL LANGUAGE plpgsql IS 'x'", + "COMMENT ON COLLATION app.\"de-DE\" IS 'x'", "COMMENT ON ACCESS METHOD heap2 IS 'x'", + "COMMENT ON STATISTICS app.st IS 'x'", + "COMMENT ON PROCEDURE app.p(IN a integer, INOUT b text) IS 'x'", + "COMMENT ON ROUTINE r() IS 'x'", + "COMMENT ON AGGREGATE percentile_disc(float8 ORDER BY anyelement) IS 'x'", + "COMMENT ON CAST (bigint AS int4) IS 'x'", + "COMMENT ON OPERATOR + (integer, integer) IS 'op'", + "COMMENT ON OPERATOR - (NONE, integer) IS 'prefix op'", + "COMMENT ON OPERATOR pg_catalog.->> (jsonb, text) IS 'x'", + "COMMENT ON OPERATOR app.-|- (int4range, int4range) IS NULL", + "COMMENT ON OPERATOR CLASS int4_ops USING btree IS 'opclass'", + "COMMENT ON OPERATOR FAMILY integer_ops USING btree IS 'opfamily'", + "COMMENT ON LARGE OBJECT 4242 IS 'lo'", + "COMMENT ON TEXT SEARCH CONFIGURATION english IS 'tscfg'", + "COMMENT ON TEXT SEARCH DICTIONARY english_stem IS 'tsdict'", + "COMMENT ON TEXT SEARCH PARSER \"default\" IS 'tsparser'", + "COMMENT ON TEXT SEARCH TEMPLATE simple IS 'tstemplate'", + "COMMENT ON CONVERSION utf8_to_iso_8859_1 IS 'conversion'", + "COMMENT ON TRANSFORM FOR int LANGUAGE sql IS 'transform'"}) + void parsesAndDeparsesWithAndWithoutPostgreSqlDialect(String sql) throws Exception { + assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertSqlCanBeParsedAndDeparsed(sql, false, null); + } + + @Test + void kindWordsRemainIdentifiers() throws Exception { + assertSqlCanBeParsedAndDeparsed("SELECT role, rule, server, extension, routine, " + + "statistics, collation, publication, subscription, language, procedural, " + + "access, method, wrapper FROM extension"); + Comment rule = roundTrip("COMMENT ON RULE rule ON rule IS NULL").get(0); + assertEquals("rule", rule.getTarget().getName().getName()); + assertEquals("rule", rule.getTarget().getRelation().getName()); + assertEquals("server", roundTrip("COMMENT ON SERVER server IS NULL").get(0).getTarget() + .getName().getName()); + assertEquals("role", roundTrip("COMMENT ON TABLE role IS NULL").get(0).getTable() + .getName()); + assertEquals("server.language", roundTrip("COMMENT ON COLUMN server.language IS NULL") + .get(0).getColumn().getFullyQualifiedName()); + } + + @Test + void operatorAndTextSearchKindWordsRemainIdentifiers() throws Exception { + assertSqlCanBeParsedAndDeparsed("SELECT operator, class, family, large, object, " + + "conversion, transform, configuration, dictionary, parser, template, search, " + + "none FROM operator"); + assertEquals("operator", roundTrip("COMMENT ON TABLE operator IS NULL").get(0).getTable() + .getName()); + assertEquals("conversion.transform", + roundTrip("COMMENT ON COLUMN conversion.transform IS NULL").get(0).getColumn() + .getFullyQualifiedName()); + assertEquals("template", roundTrip("COMMENT ON TEXT SEARCH TEMPLATE template IS NULL") + .get(0).getTarget().getName().getName()); + assertEquals("conversion", roundTrip("COMMENT ON CONVERSION conversion IS NULL").get(0) + .getTarget().getName().getName()); + assertEquals("operator", + roundTrip("COMMENT ON OPERATOR operator.+ (operator, NONE) IS NULL") + .get(0).getTarget().getOperator().getSchemaName()); + } + + @Test + void rendersProgrammaticTargets() { + CommentTarget trigger = new CommentTarget(); + trigger.setKind(CommentTarget.Kind.TRIGGER); + trigger.setName(new Table("trg")); + trigger.setRelation(new Table("app", "t")); + assertEquals("COMMENT ON TRIGGER trg ON app.t IS 'body'", + new Comment().withTarget(trigger).withComment(new StringValue("body")).toString()); + + CommentTarget cast = new CommentTarget(); + cast.setKind(CommentTarget.Kind.CAST); + cast.setSourceType(new ColDataType("integer")); + cast.setTargetType(new ColDataType("text")); + assertEquals("COMMENT ON CAST (integer AS text) IS NULL", + new Comment().withTarget(cast).toString()); + + CommentTarget named = new CommentTarget(); + named.setKind(CommentTarget.Kind.ROLE); + named.setName(new Table("r")); + named.setProcedural(true); + assertEquals("ROLE r", named.toString()); + named.setKind(CommentTarget.Kind.LANGUAGE); + assertEquals("PROCEDURAL LANGUAGE r", named.toString()); + + CommentTarget operator = new CommentTarget(); + operator.setKind(CommentTarget.Kind.OPERATOR); + operator.setOperator(new ExclusionOperator().withSchemaName("app").withName("->>")); + operator.setLeftOperandType(new ColDataType("jsonb")); + assertEquals("OPERATOR app.->> (jsonb, NONE)", operator.toString()); + + CommentTarget operatorClass = new CommentTarget(); + operatorClass.setKind(CommentTarget.Kind.OPERATOR_FAMILY); + operatorClass.setName(new Table("app", "ops")); + operatorClass.setAccessMethod("gist"); + assertEquals("OPERATOR FAMILY app.ops USING gist", operatorClass.toString()); + + CommentTarget largeObject = new CommentTarget(); + largeObject.setKind(CommentTarget.Kind.LARGE_OBJECT); + largeObject.setLargeObjectOid(4242L); + assertEquals("COMMENT ON LARGE OBJECT 4242 IS NULL", + new Comment().withTarget(largeObject).toString()); + + CommentTarget transform = new CommentTarget(); + transform.setKind(CommentTarget.Kind.TRANSFORM); + transform.setTransformType(new ColDataType("hstore")); + transform.setLanguage("plpython3u"); + assertEquals("TRANSFORM FOR hstore LANGUAGE plpython3u", transform.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"INDEX", "MATERIALIZED obj", "MATERIALIZED VIEW", "FUNCTION f(*)", + "FUNCTION f(integer ORDER BY integer)", "FUNCTION f(IN)", + "FUNCTION f(x integer DEFAULT 1)", + "CONSTRAINT ck", "CONSTRAINT ck ON", + "CONSTRAINT public.ck ON t", + "TRIGGER trg", "TRIGGER trg ON", "TRIGGER app.trg ON t", "TRIGGER trg ON DOMAIN d", + "POLICY p", "RULE r ON DOMAIN d", "PROCEDURE p(*)", + "ROUTINE r(integer ORDER BY integer)", "FUNCTION f(ORDER BY integer)", + "AGGREGATE agg", "CAST (integer)", "CAST integer AS text", "CAST (integer AS text", + "FOREIGN ft", "FOREIGN DATA fdw", "EVENT et", "ACCESS heap", "ACCESS METHOD", + "PROCEDURAL plpgsql", "LANGUAGE", "ROLE", + "OPERATOR", "OPERATOR +", "OPERATOR + ()", "OPERATOR + (integer)", + "OPERATOR + (NONE, NONE)", "OPERATOR + (integer, integer, integer)", + "OPERATOR + integer, integer", "OPERATOR app (integer, integer)", + "OPERATOR app. (integer, integer)", "OPERATOR => (integer, integer)", + "OPERATOR := (integer, integer)", "OPERATOR OPERATOR(pg_catalog.+) (integer, integer)", + "OPERATOR CLASS ops", "OPERATOR CLASS ops USING", "OPERATOR FAMILY ops USING a.b", + "OPERATOR CLASS USING btree", "LARGE OBJECT", "LARGE 42", "LARGE OBJECT lo", + "LARGE OBJECT -1", "LARGE OBJECT 4294967296", "LARGE OBJECT 99999999999999999999", + "LARGE OBJECT 1.5", "LARGE OBJECT 1, 2", "TEXT SEARCH", "TEXT SEARCH english", + "TEXT SEARCH INDEX english", "TEXT CONFIGURATION english", + "TINYTEXT SEARCH PARSER p", "CONVERSION", "TRANSFORM", "TRANSFORM int LANGUAGE sql", + "TRANSFORM FOR int", "TRANSFORM FOR int LANGUAGE", "TRANSFORM FOR int LANGUAGE a.b", + "UNKNOWN obj"}) + void rejectsIncompleteOrInvalidTargets(String target) { + assertThrows(JSQLParserException.class, () -> parse("COMMENT ON " + target + " IS NULL")); + } + + @Test + void findsOnlyRelationsExplicitlyNamedByComments() throws Exception { + for (String target : List.of("INDEX idx", "SCHEMA app", "SEQUENCE seq", "DOMAIN d", + "TYPE ty", "FUNCTION f(int)", "PROCEDURE app.p(int)", "ROUTINE r", + "AGGREGATE a(*)", "CAST (integer AS text)", "COLLATION app.c", + "STATISTICS app.st", "ACCESS METHOD am", "DATABASE d", "EVENT TRIGGER et", + "EXTENSION e", "FOREIGN DATA WRAPPER w", "LANGUAGE l", "PUBLICATION p", "ROLE r", + "SERVER s", "SUBSCRIPTION s", "TABLESPACE ts", "OPERATOR app.+ (integer, NONE)", + "OPERATOR CLASS app.t USING btree", "OPERATOR FAMILY app.t USING btree", + "LARGE OBJECT 42", "CONVERSION app.t", "TEXT SEARCH CONFIGURATION app.t", + "TEXT SEARCH DICTIONARY app.t", "TEXT SEARCH PARSER app.t", + "TEXT SEARCH TEMPLATE app.t", "TRANSFORM FOR app.t LANGUAGE sql")) { + assertTrue(new TablesNamesFinder().getTables(parse("COMMENT ON " + target + " IS NULL")) + .isEmpty()); + } + for (String target : List.of("TABLE app.t", "COLUMN app.t.c", "VIEW app.t", + "MATERIALIZED VIEW app.t", "CONSTRAINT ck ON app.t", "FOREIGN TABLE app.t", + "TRIGGER trg ON app.t", "POLICY p ON app.t", "RULE r ON app.t")) { + assertEquals(Set.of("app.t"), + new TablesNamesFinder().getTables(parse("COMMENT ON " + target + " IS NULL"))); + } + } + + @Test + void defaultConfigurationAcceptsUnambiguousTargetsButTagsRemainOptIn() throws Exception { + Comment plain = (Comment) CCJSqlParserUtil.parse("COMMENT ON INDEX idx IS 'body'"); + assertEquals(CommentTarget.Kind.INDEX, plain.getTarget().getKind()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("COMMENT ON INDEX idx IS $tag$body$tag$")); + Comment tagged = (Comment) CCJSqlParserUtil.parse("COMMENT ON INDEX idx IS $tag$body$tag$", + p -> p.withDollarQuotedStringTags(true)); + assertEquals("body", tagged.getComment().getValue()); + } + + @Test + void keepsFollowingStatementsOutsideCommentBodies() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + "COMMENT ON FUNCTION f() IS $tag$body; SELECT 0$tag$; COMMENT ON INDEX idx IS NULL; SELECT 42", + p -> p.withDialect(Dialect.POSTGRESQL)); + assertEquals(3, statements.size()); + assertEquals("body; SELECT 0", ((Comment) statements.get(0)).getComment().getValue()); + assertEquals("SELECT 42", statements.get(2).toString()); + } + + @Test + void retainsLegacyAccessorsAndAllowsSwitchingTargets() throws Exception { + Comment comment = parse("COMMENT ON INDEX idx IS 'body'"); + CommentTarget target = comment.getTarget(); + comment.setTable(new Table("t")); + assertNull(comment.getTarget()); + assertEquals("COMMENT ON TABLE t IS 'body'", comment.toString()); + comment.withTarget(target); + assertNull(comment.getTable()); + assertEquals("COMMENT ON INDEX idx IS 'body'", comment.toString()); + comment.setColumn(new Column(new Table("t"), "c")); + assertNull(comment.getTarget()); + assertEquals("COMMENT ON COLUMN t.c IS 'body'", comment.toString()); + } + + @Test + void expressionVisitorsReceiveCommentLiteralAndContext() throws Exception { + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }; + parse("COMMENT ON INDEX idx IS 'body'").accept( + new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), "context"); + assertEquals(List.of("body"), values); + } + + @ParameterizedTest + @ValueSource(strings = {"CONSTRAINT ck ON", "TRIGGER trg ON", "POLICY p ON", "RULE r ON", + "FOREIGN TABLE", "MATERIALIZED VIEW"}) + void customDeparsersCanRewriteRelationsAndLiterals(String target) throws Exception { + Comment comment = parse("COMMENT ON " + target + " app.t IS 'body'"); + StringBuilder builder = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + assertEquals("context", context); + return getBuilder().append("'changed'"); + } + }; + SelectDeParser selects = new SelectDeParser() { + @Override + public StringBuilder visit(Table table, S context) { + assertEquals("context", context); + return getBuilder().append("app.new_t"); + } + }; + comment.accept(new StatementDeParser(expressions, selects, builder), "context"); + assertEquals("COMMENT ON " + target + " app.new_t IS 'changed'", builder.toString()); + assertEquals("COMMENT ON " + target + " app.t IS 'body'", comment.toString()); + } + + private static Comment parse(String sql) throws JSQLParserException { + return (Comment) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static List roundTrip(String sql) throws JSQLParserException { + Comment comment = parse(sql); + StringBuilder builder = new StringBuilder(); + comment.accept(new StatementDeParser(builder), null); + assertEquals(comment.toString(), builder.toString()); + return List.of(comment, parse(comment.toString()), parse(builder.toString())); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/comment/CommentTest.java b/src/test/java/net/sf/jsqlparser/statement/comment/CommentTest.java index 840905bdd2..35bdd8a7b6 100755 --- a/src/test/java/net/sf/jsqlparser/statement/comment/CommentTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/comment/CommentTest.java @@ -78,7 +78,36 @@ public void testCommentColumnDeparse() throws JSQLParserException { @Test public void testToString() { Comment comment = new Comment(); - assertEquals("COMMENT ON IS null", comment.toString()); + assertEquals("COMMENT ON IS NULL", comment.toString()); + } + + @Test + public void testCommentTableIsNull() throws JSQLParserException { + String statement = "COMMENT ON TABLE schema1.table1 IS NULL"; + Comment comment = (Comment) CCJSqlParserUtil.parse(statement); + assertEquals("table1", comment.getTable().getName()); + assertThat(comment.getComment()).isNull(); + assertSqlCanBeParsedAndDeparsed(statement); + } + + @Test + public void testCommentColumnIsNull() throws JSQLParserException { + String statement = "COMMENT ON COLUMN table1.column1 IS NULL"; + Comment comment = (Comment) CCJSqlParserUtil.parse(statement); + assertEquals("column1", comment.getColumn().getColumnName()); + assertThat(comment.getComment()).isNull(); + assertSqlCanBeParsedAndDeparsed(statement); + } + + @Test + public void testCommentViewIsNull() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("COMMENT ON VIEW myschema.myView IS NULL"); + } + + @Test + public void testCommentIsNullLowercase() throws JSQLParserException { + Comment comment = (Comment) CCJSqlParserUtil.parse("comment on table table1 is null"); + assertThat(comment.getComment()).isNull(); } @Test diff --git a/src/test/java/net/sf/jsqlparser/statement/comment/PostgreSqlCommentLiteralTest.java b/src/test/java/net/sf/jsqlparser/statement/comment/PostgreSqlCommentLiteralTest.java new file mode 100644 index 0000000000..64190a76db --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/comment/PostgreSqlCommentLiteralTest.java @@ -0,0 +1,122 @@ +/*- + * #%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.statement.comment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlCommentLiteralTest { + static Stream dollarBodies() { + return Stream.of(Arguments.of("", "body"), Arguments.of("tag", "body"), + Arguments.of("Tag_123", ""), Arguments.of("_", "quote's \"double\" \\path"), + Arguments.of("한글", "한글 본문"), Arguments.of("étiquette", "$other$x$other$ $$ $1"), + Arguments.of("tag", "line1\nline2; --sql\n/* comment */"), + Arguments.of("tag", "$Tag$body$Tag$")); + } + + @ParameterizedTest + @MethodSource("dollarBodies") + void preservesDollarBodyAndTagAcrossCommentTargets(String tag, String body) throws Exception { + String quote = "$" + tag + "$"; + for (String target : List.of("TABLE t", "COLUMN t.c", "VIEW v")) { + String sql = "COMMENT ON " + target + " IS " + quote + body + quote; + Comment comment = parse(sql); + StringBuilder buffer = new StringBuilder(); + comment.accept(new StatementDeParser(buffer), null); + assertEquals(sql, buffer.toString()); + for (Comment tree : List.of(comment, parse(comment.toString()), + parse(buffer.toString()))) { + assertEquals(body, tree.getComment().getValue()); + assertEquals(quote, tree.getComment().getQuoteStr()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"'one'\n 'two'", "'one' -- line comment\n 'two'", + "'one'\r\n 'two'"}) + void concatenatesNewlineSeparatedOrdinaryLiterals(String literal) throws Exception { + Comment comment = parse("COMMENT ON TABLE t IS " + literal); + assertEquals("onetwo", comment.getComment().getValue()); + assertEquals("onetwo", parse(comment.toString()).getComment().getValue()); + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT " + literal, + p -> p.withDialect(Dialect.POSTGRESQL)); + assertEquals("onetwo", select.getSelectItem(0).getExpression(StringValue.class).getValue()); + } + + @ParameterizedTest + @ValueSource(strings = {"E'quote\\'s'", "e'quote\\'s'", "E'a\\nb'", "E'\\\\path'"}) + void acceptsEscapeStringsWithoutChangingOrdinaryStringMode(String literal) throws Exception { + Comment comment = parse("COMMENT ON TABLE t IS " + literal); + assertEquals("E", comment.getComment().getPrefix()); + assertEquals(comment.getComment().toString(), + parse(comment.toString()).getComment().toString()); + Statements statements = CCJSqlParserUtil.parseStatements( + "COMMENT ON TABLE t IS " + literal + "; SELECT '\\' AS backslash; SELECT 2", + p -> p.withDialect(Dialect.POSTGRESQL)); + assertEquals(3, statements.size()); + StringValue ordinary = + ((PlainSelect) statements.get(1)).getSelectItem(0).getExpression(StringValue.class); + assertEquals("\\", ordinary.getValue()); + assertNull(ordinary.getPrefix()); + } + + @Test + void keepsSemicolonsInsideDollarBodiesAndFollowingStatements() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + "COMMENT ON TABLE t IS $tag$body; SELECT 0; --text\n$tag$; SELECT 42;", + p -> p.withDialect(Dialect.POSTGRESQL)); + assertEquals(2, statements.size()); + assertEquals("body; SELECT 0; --text\n", + ((Comment) statements.get(0)).getComment().getValue()); + assertEquals("SELECT 42", statements.get(1).toString()); + } + + @Test + void keepsTaggedQuotesOptInAndSupportsExplicitFeature() throws Exception { + String sql = "COMMENT ON TABLE t IS $tag$body$tag$"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.POSTGRESQL).withDollarQuotedStringTags(false))); + Comment comment = + (Comment) CCJSqlParserUtil.parse(sql, p -> p.withDollarQuotedStringTags(true)); + assertEquals("body", comment.getComment().getValue()); + assertNull(parse("COMMENT ON TABLE t IS NULL").getComment()); + assertEquals("", parse("COMMENT ON TABLE t IS ''").getComment().getValue()); + } + + @ParameterizedTest + @ValueSource(strings = {"'one' 'two'", "$t$one$t$\n $t$two$t$", "$t$one$t$\n 'two'", + "'one'\n $t$two$t$", "'one'\n E'two'", "$Tag$body$tag$", "$tag$unterminated", + "$1$body$1$"}) + void rejectsInvalidLiteralForms(String literal) { + assertThrows(JSQLParserException.class, () -> parse("COMMENT ON TABLE t IS " + literal)); + } + + private static Comment parse(String sql) throws JSQLParserException { + return (Comment) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnAttributeTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnAttributeTest.java new file mode 100644 index 0000000000..761976862c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnAttributeTest.java @@ -0,0 +1,241 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNotAllowed; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.GeneratedColumnDefinition; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ColumnAttributeTest { + static Stream attributes() { + return Stream.of( + Arguments.of("NOT NULL", ColumnOption.Kind.NULLABILITY), + Arguments.of("NULL", ColumnOption.Kind.NULLABILITY), + Arguments.of("AUTO_INCREMENT", ColumnOption.Kind.AUTO_INCREMENT), + Arguments.of("VISIBLE", ColumnOption.Kind.VISIBILITY), + Arguments.of("INVISIBLE", ColumnOption.Kind.VISIBILITY), + Arguments.of("COLLATE utf8mb4_bin", ColumnOption.Kind.COLLATE), + Arguments.of("COLLATE `a.b`", ColumnOption.Kind.COLLATE), + Arguments.of("COMMENT 'quote''s \\n body'", ColumnOption.Kind.COMMENT), + Arguments.of("COMMENT ''", ColumnOption.Kind.COMMENT), + Arguments.of("ON UPDATE CURRENT_TIMESTAMP", ColumnOption.Kind.ON_UPDATE), + Arguments.of("ON UPDATE CURRENT_TIMESTAMP(6)", ColumnOption.Kind.ON_UPDATE), + Arguments.of("ON UPDATE NOW()", ColumnOption.Kind.ON_UPDATE), + Arguments.of("PRIMARY KEY", ColumnOption.Kind.CONSTRAINT)); + } + + @ParameterizedTest + @MethodSource("attributes") + void createAndAlterStructureAttributes(String attribute, ColumnOption.Kind kind) + throws Exception { + for (String sql : statements("VARCHAR(20) " + attribute)) { + for (Statement statement : roundTrip(sql, Dialect.MYSQL)) { + ColumnOption option = column(statement).getColumnOptions().get(0); + assertEquals(kind, option.getKind()); + assertEquals(attribute, option.toString()); + assertNull(option.getDefaultExpression()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"x + 1", "COALESCE(x, 0) * 2", "CASE WHEN x > 0 THEN x ELSE 0 END", + "CAST(x AS CHAR(10))", "CONCAT('prefix,', x)", "((x + 1) * 2)"}) + void generatedExpressionsKeepPrefixStorageAndFollowingOptions(String expression) + throws Exception { + for (String prefix : List.of("", "GENERATED ALWAYS ")) { + for (String storage : List.of("", " STORED", " VIRTUAL")) { + for (String sql : statements("INT " + prefix + "AS (" + expression + ")" + storage + + " NOT NULL COMMENT 'body'")) { + for (Statement statement : roundTrip(sql, Dialect.MYSQL)) { + List options = column(statement).getColumnOptions(); + assertEquals( + List.of(ColumnOption.Kind.GENERATED, ColumnOption.Kind.NULLABILITY, + ColumnOption.Kind.COMMENT), + options.stream().map(ColumnOption::getKind) + .collect(Collectors.toList())); + GeneratedColumnDefinition generated = + options.get(0).getGeneratedDefinition(); + assertEquals(!prefix.isEmpty(), generated.isGeneratedAlways()); + assertEquals( + storage.isEmpty() ? null + : GeneratedColumnDefinition.Storage.valueOf(storage.trim()), + generated.getStorage()); + assertEquals(CCJSqlParserUtil.parseExpression(expression).toString(), + generated.getExpression().toString()); + } + } + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"\"C\"", "pg_catalog.\"C\"", "\"s.p\".\"a.b\""}) + void postgresCollationAndCharsetAreSeparateAttributes(String name) throws Exception { + for (Statement statement : roundTrip( + "CREATE TABLE t (c TEXT COLLATE " + name + " NOT NULL)", Dialect.POSTGRESQL)) { + assertEquals(name, column(statement).getColumnOptions().get(0).getCollation()); + } + Statement mysql = + parse("CREATE TABLE t (c VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin)", + Dialect.MYSQL); + assertEquals("utf8mb4", column(mysql).getColDataType().getCharacterSet()); + assertEquals("utf8mb4_bin", column(mysql).getColumnOptions().get(0).getCollation()); + } + + @Test + void keepsIdentityReferencesDefaultsAndRawExtensionsDistinct() throws Exception { + Statement statement = parse( + "CREATE TABLE t (c INT NOT NULL DEFAULT 0 ON UPDATE CURRENT_TIMESTAMP(6) " + + "REFERENCES parent(id) ON UPDATE CASCADE COMMENT 'body' COLUMN_FORMAT FIXED " + + "NOT SECONDARY)", + Dialect.MYSQL); + List options = column(statement).getColumnOptions(); + assertEquals(List.of(ColumnOption.Kind.NULLABILITY, ColumnOption.Kind.DEFAULT, + ColumnOption.Kind.ON_UPDATE, ColumnOption.Kind.REFERENCE, + ColumnOption.Kind.COMMENT, ColumnOption.Kind.COLUMN_FORMAT), + options.subList(0, 6).stream().map(ColumnOption::getKind) + .collect(Collectors.toList())); + assertTrue(options.stream().skip(6).allMatch(o -> o.getKind() == ColumnOption.Kind.OTHER)); + for (String definition : List.of("INT GENERATED ALWAYS AS IDENTITY", + "INT GENERATED BY DEFAULT AS IDENTITY")) { + assertEquals(ColumnOption.Kind.IDENTITY, + column(parse("CREATE TABLE t (c " + definition + ")", Dialect.POSTGRESQL)) + .getColumnOptions().get(0).getKind()); + } + } + + @Test + void mutationsUpdateLegacyTokensAndBothRenderers() throws Exception { + Statement statement = + parse("CREATE TABLE t (c INT NOT NULL INVISIBLE COMMENT 'old' AS (x + 1) STORED)", + Dialect.MYSQL); + List options = column(statement).getColumnOptions(); + options.get(0).setNullable(true); + options.get(1).setVisible(true); + options.get(2).setComment(new StringValue("'new'")); + options.get(3).getGeneratedDefinition().setExpression(new LongValue(42)); + options.get(3).getGeneratedDefinition() + .setStorage(GeneratedColumnDefinition.Storage.VIRTUAL); + assertEquals(List.of("NULL", "VISIBLE", "COMMENT", "'new'", "AS", "(42)", "VIRTUAL"), + column(statement).getColumnSpecs()); + roundTrip(statement.toString(), Dialect.MYSQL); + column(statement).addColumnSpecs("CUSTOM"); + assertEquals(ColumnOption.Kind.OTHER, options.get(options.size() - 1).getKind()); + column(statement).setColumnSpecs(List.of("NOT", "NULL")); + assertEquals(ColumnOption.Kind.OTHER, + column(statement).getColumnOptions().get(0).getKind()); + assertEquals("CREATE TABLE t (c INT NOT NULL)", statement.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (c INT AS (1 + 2) COMMENT 'body')", + "ALTER TABLE t ADD c INT AS (1 + 2) COMMENT 'body'"}) + void visitorsAndCustomDeparsersReachExpressionsWithContext(String sql) throws Exception { + Statement statement = parse(sql, Dialect.MYSQL); + List visited = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("ctx", context); + visited.add(value.toString()); + return null; + } + + @Override + public Void visit(StringValue value, S context) { + assertEquals("ctx", context); + visited.add(value.getValue()); + return null; + } + }; + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "ctx"); + assertEquals(List.of("1", "2", "body"), visited); + StringBuilder builder = new StringBuilder(); + ExpressionDeParser printer = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 10); + } + }; + statement.accept(new StatementDeParser(printer, new SelectDeParser(), builder), null); + assertEquals(statement.toString().replace("1 + 2", "11 + 12"), builder.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"AS (?)", "ON UPDATE ?"}) + void validationReachesNewExpressions(String attribute) { + validateNotAllowed("CREATE TABLE t (c INT " + attribute + ")", 1, 1, FeaturesAllowed.DDL, + Feature.jdbcParameter); + } + + @ParameterizedTest + @ValueSource( + strings = {"COMMENT", "COLLATE", "ON UPDATE", "AS (1 +)", "GENERATED ALWAYS AS (1 +)"}) + void rejectsIncompleteStructuredAttributes(String attribute) { + assertThrows(JSQLParserException.class, + () -> parse("CREATE TABLE t (c INT " + attribute + ")", Dialect.MYSQL)); + } + + private static List statements(String definition) { + return List.of("CREATE TABLE t (c " + definition + ")", + "ALTER TABLE t ADD COLUMN c " + definition, + "ALTER TABLE t MODIFY COLUMN c " + definition, + "ALTER TABLE t CHANGE COLUMN old_c c " + definition); + } + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + } + + private static ColumnDefinition column(Statement statement) { + return statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0); + } + + private static List roundTrip(String sql, Dialect dialect) + throws JSQLParserException { + Statement statement = parse(sql, dialect); + StringBuilder builder = new StringBuilder(); + statement.accept(new StatementDeParser(builder), null); + assertEquals(statement.toString(), builder.toString()); + return List.of(statement, parse(statement.toString(), dialect), + parse(builder.toString(), dialect)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java new file mode 100644 index 0000000000..2e403e5524 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java @@ -0,0 +1,303 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNotAllowed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.CollateExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterDomain; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.create.domain.CreateDomain; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ColumnDefaultExpressionTest { + @ParameterizedTest + @ValueSource(strings = {"1", "-1", "NULL", "true", "CURRENT_TIMESTAMP(6)", + "nextval('app.counter'::regclass)", "'value'::character varying", "ARRAY[1, 2]::int[]", + "(1 + 2 * 3)", "(UUID_TO_BIN(UUID()))", "(JSON_ARRAY())", + "(CURRENT_DATE + INTERVAL 1 YEAR)", "CASE WHEN 1 = 1 THEN 2 ELSE 3 END", + "1 > 0", "1 NOT IN (2, 3)"}) + void createAndAlterKeepDefaultsSeparateFromFollowingConstraints(String value) + throws JSQLParserException { + Expression expected = CCJSqlParserUtil.parseExpression(value); + for (String prefix : Arrays.asList("CREATE TABLE t (a INT DEFAULT ", + "ALTER TABLE t ADD COLUMN a INT DEFAULT ", + "ALTER TABLE t MODIFY COLUMN a INT DEFAULT ")) { + Statement statement = CCJSqlParserUtil.parse(prefix + value + " NOT NULL" + + (prefix.startsWith("CREATE") ? ", b INT)" : ", ADD COLUMN b INT")); + ColumnDefinition column = firstColumn(statement); + ColumnOption option = defaultOption(column); + assertEquals(expected.getClass(), option.getDefaultExpression().getClass()); + assertEquals(expected.toString(), option.getDefaultExpression().toString()); + assertThat(column.getColumnSpecs()).containsExactly("DEFAULT", expected.toString(), + "NOT", "NULL"); + assertRoundTrip(statement); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (a INT DEFAULT (1 + 2), b INT DEFAULT 3)", + "ALTER TABLE t ADD (a INT DEFAULT (1 + 2), b INT DEFAULT 3)", + "ALTER TABLE t MODIFY (a INT DEFAULT (1 + 2), b INT DEFAULT 3)", + "ALTER TABLE t ADD a INT DEFAULT (1 + 2), MODIFY b INT DEFAULT 3"}) + void visitorReachesEachDefaultOnceWithContext(String sql) throws JSQLParserException { + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }; + CCJSqlParserUtil.parse(sql) + .accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "context"); + assertThat(values).containsExactly(1L, 2L, 3L); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (a INT DEFAULT (1 + 2) NOT NULL)", + "CREATE TABLE t OF row_type (PRIMARY KEY (a), a WITH OPTIONS DEFAULT (1 + 2))", + "ALTER TABLE t ADD COLUMN IF NOT EXISTS a INT DEFAULT (1 + 2)", + "ALTER TABLE t ADD (a INT DEFAULT 1, b INT DEFAULT 2)", + "ALTER TABLE t MODIFY (a INT DEFAULT 1, b INT DEFAULT 2)", + "ALTER TABLE t MODIFY (a INT DEFAULT (1 + 2))", + "ALTER TABLE t CHANGE COLUMN old_a a INT DEFAULT (1 + 2) AFTER id", + "ALTER TABLE t MODIFY COLUMN a INT DEFAULT (1 + 2) FIRST", + "ALTER TABLE t ALTER COLUMN a TYPE INT USING (1 + 2)"}) + void sharedRenderingUsesCustomExpressionPrinterAndPreservesAlterLayout(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(statement.toString().replace("1", "101").replace("2", "102"), + output.toString()); + assertRoundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (a INT DEFAULT (1 + 2) NOT NULL)", + "ALTER TABLE t ADD COLUMN a INT DEFAULT (1 + 2) NOT NULL"}) + void replacingDefaultUpdatesLegacyTokensAndBothRenderers(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + ColumnDefinition column = firstColumn(statement); + defaultOption(column).setDefaultExpression(new LongValue(42)); + assertThat(column.getColumnSpecs()).containsExactly("DEFAULT", "42", "NOT", "NULL"); + assertEquals(sql.replace("(1 + 2)", "42"), statement.toString()); + assertRoundTrip(statement); + column.addColumnSpecs("UNIQUE"); + assertNotNull(defaultOption(column).getDefaultExpression()); + assertRoundTrip(statement); + column.setColumnSpecs(Arrays.asList("DEFAULT", "9")); + assertEquals(ColumnOption.Kind.OTHER, column.getColumnOptions().get(0).getKind()); + assertEquals(sql.replace("(1 + 2) NOT NULL", "9"), statement.toString()); + assertRoundTrip(statement); + } + + @Test + void identityAndSerialDefaultsKeepTheirOwnOptionKinds() throws JSQLParserException { + ColumnDefinition identity = firstColumn(CCJSqlParserUtil.parse( + "CREATE TABLE t (a INT GENERATED BY DEFAULT AS IDENTITY)")); + assertEquals(ColumnOption.Kind.IDENTITY, identity.getColumnOptions().get(0).getKind()); + assertNull(identity.getColumnOptions().get(0).getDefaultExpression()); + ColumnDefinition serial = firstColumn(CCJSqlParserUtil.parse( + "CREATE TABLE t (a INT SERIAL DEFAULT VALUE)")); + assertEquals(ColumnOption.Kind.SERIAL_DEFAULT_VALUE, + serial.getColumnOptions().get(0).getKind()); + assertNull(serial.getColumnOptions().get(0).getDefaultExpression()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (a TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)", + "CREATE TABLE t (a INT DEFAULT ON NULL 1 NOT NULL)", + "CREATE TABLE t (a INT WITH DEFAULT)", + "CREATE TABLE t (a INT DEFAULT NOT NULL)", + "CREATE TABLE t (a INT NOT NULL WITH DEFAULT, b INT DEFAULT)", + "CREATE TABLE t (a INT CONSTRAINT df DEFAULT 1 REFERENCES parent(id))", + "ALTER TABLE t MODIFY (COLUMN a DROP DEFAULT, COLUMN b DROP DEFAULT)"}) + void neighboringAndLegacyColumnOptionsStillRoundTrip(String sql) throws JSQLParserException { + assertRoundTrip(CCJSqlParserUtil.parse(sql)); + } + + @ParameterizedTest + @ValueSource( + strings = {"CREATE TABLE t (c VARCHAR(30) ", "ALTER TABLE t ADD COLUMN c VARCHAR(30) ", + "ALTER TABLE t MODIFY COLUMN c VARCHAR(30) ", + "ALTER TABLE t CHANGE COLUMN old_c c VARCHAR(30) "}) + void collateAfterDefaultIsAColumnAttribute(String prefix) throws JSQLParserException { + String sql = prefix + "NOT NULL DEFAULT 'N' COLLATE utf8mb4_unicode_ci COMMENT 'flag'" + + (prefix.startsWith("CREATE") ? ")" : ""); + List> configurations = Arrays.asList(parser -> { + }, parser -> parser.withDialect(Dialect.MYSQL)); + for (Consumer configuration : configurations) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, configuration); + ColumnDefinition column = firstColumn(statement); + assertThat(column.getColumnOptions()).extracting(ColumnOption::getKind) + .containsExactly(ColumnOption.Kind.NULLABILITY, ColumnOption.Kind.DEFAULT, + ColumnOption.Kind.COLLATE, ColumnOption.Kind.COMMENT); + assertInstanceOf(StringValue.class, defaultOption(column).getDefaultExpression()); + assertEquals("utf8mb4_unicode_ci", column.getColumnOptions().get(2).getCollation()); + assertThat(column.getColumnSpecs()).containsExactly("NOT", "NULL", "DEFAULT", "'N'", + "COLLATE", "utf8mb4_unicode_ci", "COMMENT", "'flag'"); + assertRoundTrip(statement); + } + } + + static Stream defaultValuesBeforeCollate() { + return Stream.of(Arguments.of(Dialect.MYSQL, "_utf8mb4'N'", "utf8mb4_bin"), + Arguments.of(Dialect.MYSQL, "'N'", "'utf8mb4_bin'"), + Arguments.of(Dialect.MYSQL, "1", "utf8mb4_bin"), + Arguments.of(Dialect.MYSQL, "-1", "utf8mb4_bin"), + Arguments.of(Dialect.POSTGRESQL, "'a' || 'b'", "\"C\""), + Arguments.of(Dialect.POSTGRESQL, "lower('A' COLLATE \"C\")", "\"C\""), + Arguments.of(Dialect.POSTGRESQL, "CASE WHEN true THEN 'a' END", "\"C\""), + Arguments.of(Dialect.POSTGRESQL, "'x'::text", "\"C\""), + Arguments.of(Dialect.POSTGRESQL, "'x'", "pg_catalog.\"default\"")); + } + + @ParameterizedTest + @MethodSource("defaultValuesBeforeCollate") + void defaultValueEndsBeforeTopLevelCollate(Dialect dialect, String value, String collation) + throws JSQLParserException { + String definition = " VARCHAR(30) DEFAULT " + value + " COLLATE " + collation + " NOT NULL"; + String alter = dialect == Dialect.MYSQL ? "ALTER TABLE t MODIFY COLUMN c" + : "ALTER TABLE t ADD COLUMN c"; + for (String sql : Arrays.asList("CREATE TABLE t (c" + definition + ")", + alter + definition)) { + ColumnDefinition column = firstColumn(assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(dialect))); + assertThat(column.getColumnOptions()).extracting(ColumnOption::getKind) + .containsExactly(ColumnOption.Kind.DEFAULT, ColumnOption.Kind.COLLATE, + ColumnOption.Kind.NULLABILITY); + assertEquals(value, defaultOption(column).getDefaultExpression().toString()); + assertEquals(collation, column.getColumnOptions().get(1).getCollation()); + } + } + + @Test + void parenthesizedCollateStaysInTheDefaultValue() throws JSQLParserException { + ColumnDefinition column = firstColumn(assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t MODIFY COLUMN c VARCHAR(3) DEFAULT ('N' COLLATE utf8mb4_bin) NOT NULL", + true, parser -> parser.withDialect(Dialect.MYSQL))); + assertThat(column.getColumnOptions()).extracting(ColumnOption::getKind) + .containsExactly(ColumnOption.Kind.DEFAULT, ColumnOption.Kind.NULLABILITY); + Expression value = defaultOption(column).getDefaultExpression(); + assertInstanceOf(ParenthesedExpressionList.class, value); + assertInstanceOf(CollateExpression.class, ((ParenthesedExpressionList) value).get(0)); + } + + @Test + void setDefaultAndDomainDefaultsKeepTheExpressionCollate() throws JSQLParserException { + // PostgreSQL's SET DEFAULT takes an a_expr, which includes COLLATE. CREATE DOMAIN is + // unchanged. + Consumer postgres = parser -> parser.withDialect(Dialect.POSTGRESQL); + Alter table = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t ALTER COLUMN c SET DEFAULT 'x' COLLATE \"C\"", true, postgres); + assertInstanceOf(CollateExpression.class, table.getAlterExpressions().get(0) + .getColumnDefaults().get(0).getDefaultExpression()); + AlterRelation view = (AlterRelation) assertSqlCanBeParsedAndDeparsed( + "ALTER VIEW v ALTER COLUMN c SET DEFAULT 'x' COLLATE \"C\"", true, postgres); + assertInstanceOf(CollateExpression.class, + view.getActions().get(0).getDefaultExpression()); + AlterDomain alterDomain = (AlterDomain) assertSqlCanBeParsedAndDeparsed( + "ALTER DOMAIN d SET DEFAULT 'x' COLLATE \"C\"", true, postgres); + assertInstanceOf(CollateExpression.class, alterDomain.getDefaultExpression()); + CreateDomain createDomain = (CreateDomain) assertSqlCanBeParsedAndDeparsed( + "CREATE DOMAIN d AS text DEFAULT 'x' COLLATE \"C\"", true, postgres); + assertInstanceOf(CollateExpression.class, createDomain.getDefaultExpression()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (a INT DEFAULT (1 + 2", + "CREATE TABLE t (a INT DEFAULT (1 +))", + "ALTER TABLE t ADD a INT DEFAULT (1 +), ADD b INT"}) + void rejectsIncompleteDefaultExpressions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (a INT DEFAULT ?)", + "ALTER TABLE t ADD COLUMN a INT DEFAULT ?"}) + void validationChecksStructuredDefaultExpressions(String sql) { + validateNotAllowed(sql, 1, 1, FeaturesAllowed.DDL, Feature.jdbcParameter); + } + + private static Statement parse(String sql) throws JSQLParserException { + return sql.contains(" OF ") + ? CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)) + : CCJSqlParserUtil.parse(sql); + } + + private static ColumnDefinition firstColumn(Statement statement) { + return statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0); + } + + private static ColumnOption defaultOption(ColumnDefinition column) { + return column.getColumnOptions().stream() + .filter(option -> option.getKind() == ColumnOption.Kind.DEFAULT) + .findFirst().orElseThrow(AssertionError::new); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefinitionStateTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefinitionStateTest.java new file mode 100644 index 0000000000..de85eca5bd --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefinitionStateTest.java @@ -0,0 +1,227 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyReference; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ColumnDefinitionStateTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id INT COLUMN_FORMAT COMPRESSED)", + "ALTER TABLE t ADD COLUMN id INT COLUMN_FORMAT COMPRESSED", + "CREATE TABLE t (id INT DEFAULT 1)", "ALTER TABLE t ADD COLUMN id INT DEFAULT 1"}) + void specificationSnapshotsAreReadOnlyForRawAndStructuredOptions(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + ColumnDefinition column = column(statement); + List snapshot = column.getColumnSpecs(); + assertThrows(UnsupportedOperationException.class, () -> snapshot.add("NOT NULL")); + assertThrows(UnsupportedOperationException.class, () -> snapshot.set(0, "changed")); + assertThrows(UnsupportedOperationException.class, snapshot::clear); + ColumnOption option = column.getColumnOptions().get(0); + if (option.getKind() == ColumnOption.Kind.OTHER) { + assertTrue(column.getColumnOptions().stream() + .allMatch(raw -> raw.getKind() == ColumnOption.Kind.OTHER)); + ColumnOption rawOption = column.getColumnOptions().stream() + .filter(raw -> raw.getTokens().contains("COMPRESSED")) + .findFirst().orElseThrow(); + List rawTokens = new ArrayList<>(rawOption.getTokens()); + rawTokens.set(rawTokens.indexOf("COMPRESSED"), "FIXED"); + rawOption.setTokens(rawTokens); + assertEquals(List.of("COLUMN_FORMAT", "COMPRESSED"), snapshot); + assertEquals(List.of("COLUMN_FORMAT", "FIXED"), column.getColumnSpecs()); + } else { + option.setDefaultExpression(new LongValue(2)); + assertEquals(List.of("DEFAULT", "1"), snapshot); + assertEquals(List.of("DEFAULT", "2"), column.getColumnSpecs()); + } + roundTrip(statement); + } + + @Test + void constructorAndRawSettersCopyTokensWithoutParsingThem() { + List input = new ArrayList<>(List.of("DEFAULT", "1")); + ColumnDefinition column = new ColumnDefinition("id", new ColDataType("INT"), input); + input.set(1, "99"); + assertEquals("id INT DEFAULT 1", column.toString()); + assertEquals(ColumnOption.Kind.OTHER, column.getColumnOptions().get(0).getKind()); + assertNull(column.getColumnOptions().get(0).getDefaultExpression()); + List appended = new ArrayList<>(List.of("NOT", "NULL")); + column.addColumnSpecs(appended); + appended.clear(); + assertEquals("id INT DEFAULT 1 NOT NULL", column.toString()); + column.setColumnSpecs(input); + input.clear(); + assertEquals("id INT DEFAULT 99", column.toString()); + assertEquals(1, column.getColumnOptions().size()); + column.setColumnSpecs(column.getColumnSpecs()); + assertEquals("id INT DEFAULT 99", column.toString()); + } + + @Test + void constructorDoesNotDispatchToOverriddenSpecificationSetter() { + ColumnDefinition column = new ColumnDefinition("id", new ColDataType("INT"), + List.of("NOT", "NULL")) { + @Override + public void setColumnSpecs(List specs) { + fail("The constructor must not call an overridable setter"); + } + }; + assertEquals(List.of("NOT", "NULL"), column.getColumnSpecs()); + assertEquals("id INT NOT NULL", column.toString()); + } + + @Test + void optionSettersCopyMembershipAndKeepTheOriginalMutableNodes() { + ColumnOption defaultOption = ColumnOption.defaultValue(new LongValue(1)); + List input = new ArrayList<>(List.of(defaultOption)); + ColumnDefinition column = new ColumnDefinition("id", new ColDataType("INT")) + .withColumnOptions(input); + input.clear(); + assertSame(defaultOption, column.getColumnOptions().get(0)); + List liveOptions = column.getColumnOptions(); + defaultOption.setDefaultExpression(new LongValue(2)); + column.addColumnSpecs("NOT", "NULL"); + column.addColumnOptions(ColumnOption.comment( + new net.sf.jsqlparser.expression.StringValue("description"))); + assertSame(liveOptions, column.getColumnOptions()); + assertEquals(3, liveOptions.size()); + assertEquals("id INT DEFAULT 2 NOT NULL COMMENT 'description'", column.toString()); + column.setColumnOptions(List.of(defaultOption)); + column.addColumnSpecs("NOT", "NULL"); + assertEquals("id INT DEFAULT 2 NOT NULL", column.toString()); + column.getColumnOptions().remove(0); + assertEquals(List.of("NOT", "NULL"), column.getColumnSpecs()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id INT DEFAULT 1 REFERENCES parent(id))", + "ALTER TABLE t ADD COLUMN id INT DEFAULT 1 REFERENCES parent(id)"}) + void rawAppendPreservesStructuredChildrenVisitorsAndCustomRendering(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + ColumnDefinition column = column(statement); + ColumnOption defaultOption = column.getColumnOptions().get(0); + ForeignKeyReference reference = column.getForeignKeyReference(); + column.addColumnSpecs("NOT", "NULL"); + assertSame(defaultOption, column.getColumnOptions().get(0)); + assertSame(reference, column.getForeignKeyReference()); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(column, visited::add, ignored -> { + }); + assertEquals(List.of(defaultOption.getDefaultExpression()), visited); + assertEquals(Set.of("t", "parent"), new TablesNamesFinder().getTables(statement)); + StringBuilder rewritten = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(7); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), rewritten), null); + assertEquals(statement.toString().replace("DEFAULT 1", "DEFAULT 7"), rewritten.toString()); + assertEquals(rewritten.toString(), parse(rewritten.toString()).toString()); + roundTrip(statement); + + column.setColumnSpecs(List.of("DEFAULT", "2")); + assertNull(column.getForeignKeyReference()); + visited.clear(); + TableDefinitionTraversal.visit(column, visited::add, ignored -> { + }); + assertTrue(visited.isEmpty()); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(statement)); + defaultOption.setDefaultExpression(new LongValue(9)); + assertEquals(List.of("DEFAULT", "2"), column.getColumnSpecs()); + roundTrip(statement); + } + + @Test + void nullEmptyAndAppendContractsRemainDistinct() { + ColumnDefinition column = new ColumnDefinition("id", new ColDataType("INT")); + assertNull(column.getColumnSpecs()); + assertNull(column.getColumnOptions()); + assertThrows(NullPointerException.class, + () -> column.addColumnSpecs((Collection) null)); + assertThrows(NullPointerException.class, + () -> column.addColumnOptions((ColumnOption[]) null)); + assertNull(column.getColumnOptions()); + column.setColumnSpecs(List.of()); + assertTrue(column.getColumnSpecs().isEmpty()); + assertTrue(column.getColumnOptions().isEmpty()); + assertEquals("id INT", column.toString()); + column.setColumnOptions(null); + assertNull(column.getColumnSpecs()); + column.addColumnSpecs(List.of()); + assertTrue(column.getColumnSpecs().isEmpty()); + column.setColumnSpecs(null); + assertNull(column.getColumnOptions()); + column.setColumnOptions(List.of()); + assertTrue(column.getColumnSpecs().isEmpty()); + assertEquals("INT", column.toStringDataTypeAndSpec()); + } + + @Test + void parserKeepsExistingAbsentAndEmptySpecificationBoundaries() throws JSQLParserException { + ColumnDefinition create = column(parse("CREATE TABLE t (id INT)")); + ColumnDefinition alter = column(parse("ALTER TABLE t ADD COLUMN id INT")); + assertNull(create.getColumnSpecs()); + assertNull(create.getColumnOptions()); + assertTrue(alter.getColumnSpecs().isEmpty()); + assertTrue(alter.getColumnOptions().isEmpty()); + ColumnDataType dataType = new ColumnDataType(false); + assertSame(dataType, dataType.withColumnName("id").withColDataType(new ColDataType("INT")) + .withColumnSpecs(List.of("NOT", "NULL"))); + assertSame(dataType, dataType.addColumnSpecs("DEFAULT", "1")); + assertSame(dataType, dataType.addColumnSpecs(List.of("UNIQUE"))); + assertEquals("id INT NOT NULL DEFAULT 1 UNIQUE", dataType.toString()); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static ColumnDefinition column(Statement statement) { + return statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql)); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java new file mode 100644 index 0000000000..3257f93134 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java @@ -0,0 +1,92 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyReference; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; + +class ColumnOptionMutationTest { + @Test + void clearingIdentityDoesNotRestoreRawSpecifications() throws JSQLParserException { + CreateTable table = parse("id INT GENERATED ALWAYS AS IDENTITY"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + column.getColumnOptions().clear(); + assertTrue(column.getColumnSpecs().isEmpty()); + assertSql(table, "CREATE TABLE t (id INT)"); + } + + @Test + void legacyAppendPreservesReferenceAndTraversal() throws JSQLParserException { + CreateTable table = parse("id INT REFERENCES parent(id)"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + ForeignKeyReference reference = column.getForeignKeyReference(); + column.addColumnSpecs("NOT", "NULL"); + column.addColumnSpecs(Arrays.asList("DEFAULT", "1")); + assertSame(reference, column.getForeignKeyReference()); + assertTrue(new TablesNamesFinder().getTables(table).contains("parent")); + assertSql(table, "CREATE TABLE t (id INT REFERENCES parent(id) NOT NULL DEFAULT 1)"); + } + + @Test + void addingStructuredOptionPreservesExistingRawSpecifications() throws JSQLParserException { + CreateTable table = parse("id INT"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + column.setColumnSpecs(Arrays.asList("NOT", "NULL")); + assertEquals(ColumnOption.Kind.OTHER, column.getColumnOptions().get(0).getKind()); + ForeignKeyReference reference = parse("id INT REFERENCES parent(id)") + .getColumnDefinitions().get(0).getForeignKeyReference(); + column.addColumnOptions(ColumnOption.reference(reference)); + assertSql(table, "CREATE TABLE t (id INT NOT NULL REFERENCES parent(id))"); + assertTrue(new TablesNamesFinder().getTables(table).contains("parent")); + } + + @Test + void replacingOptionsWithNullClearsPreviousSpecifications() throws JSQLParserException { + CreateTable table = parse("id INT GENERATED ALWAYS AS IDENTITY"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + column.setColumnOptions(null); + assertNull(column.getColumnSpecs()); + assertSql(table, "CREATE TABLE t (id INT)"); + } + + @Test + void legacySetterStillReplacesStructuredOptions() throws JSQLParserException { + CreateTable table = parse("id INT REFERENCES parent(id)"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + column.setColumnSpecs(Arrays.asList("NOT", "NULL")); + assertNull(column.getForeignKeyReference()); + assertSql(table, "CREATE TABLE t (id INT NOT NULL)"); + } + + private static CreateTable parse(String definition) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t (" + definition + ")"); + } + + private static void assertSql(CreateTable table, String expected) throws JSQLParserException { + assertEquals(expected, table.toString()); + StringBuilder buffer = new StringBuilder(); + table.accept(new StatementDeParser(buffer), null); + assertEquals(expected, buffer.toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionTokenTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionTokenTest.java new file mode 100644 index 0000000000..289651d33d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionTokenTest.java @@ -0,0 +1,247 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; +import net.sf.jsqlparser.statement.create.table.ForeignKeyReference; +import net.sf.jsqlparser.statement.create.table.GeneratedColumnDefinition; +import net.sf.jsqlparser.statement.create.table.IdentityDefinition; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ColumnOptionTokenTest { + // Percona Server's COLUMN_FORMAT COMPRESSED is not modeled and stays opaque. + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id INT COLUMN_FORMAT COMPRESSED)", + "ALTER TABLE t ADD COLUMN id INT COLUMN_FORMAT COMPRESSED"}) + void parsedRawOptionsUseExplicitTokenReplacement(String sql) throws JSQLParserException { + Statement statement = parse(sql); + ColumnOption option = column(statement).getColumnOptions().stream() + .filter(raw -> raw.getTokens().contains("COMPRESSED")).findFirst().orElseThrow(); + List snapshot = option.getTokens(); + assertReadOnly(snapshot); + List replacement = new ArrayList<>(snapshot); + replacement.set(replacement.indexOf("COMPRESSED"), "FIXED"); + option.setTokens(replacement); + replacement.clear(); + assertTrue(snapshot.contains("COMPRESSED")); + assertTrue(option.getTokens().contains("FIXED")); + assertEquals(ColumnOption.Kind.OTHER, option.getKind()); + assertTrue(statement.toString().contains("COLUMN_FORMAT FIXED")); + assertRoundTrip(statement); + } + + @Test + void rawFactoriesAndSetterOwnTheirTokensWithoutParsingThem() { + List input = new ArrayList<>(List.of("DEFAULT", "1")); + ColumnOption option = ColumnOption.raw(input); + input.set(1, "99"); + List snapshot = option.getTokens(); + assertReadOnly(snapshot); + assertEquals(List.of("DEFAULT", "1"), snapshot); + assertNull(option.getDefaultExpression()); + assertEquals(ColumnOption.Kind.OTHER, option.getKind()); + option.setTokens(input); + input.clear(); + assertEquals("DEFAULT 99", option.toString()); + assertEquals(List.of("DEFAULT", "1"), snapshot); + option.setTokens(option.getTokens()); + assertEquals("DEFAULT 99", option.toString()); + + String[] array = {"NOT", "NULL"}; + ColumnOption fromArray = ColumnOption.raw(array); + array[0] = "changed"; + assertEquals("NOT NULL", fromArray.toString()); + assertReadOnly(fromArray.getTokens()); + } + + @Test + void unsetAndEmptyRawTokensRemainDistinct() { + ColumnOption option = ColumnOption.raw((List) null); + assertNull(option.getTokens()); + assertEquals("", option.toString()); + option.setTokens(List.of()); + List empty = option.getTokens(); + assertTrue(empty.isEmpty()); + assertThrows(UnsupportedOperationException.class, () -> empty.add("changed")); + option.setTokens(List.of("CUSTOM")); + assertTrue(empty.isEmpty()); + option.setTokens(null); + assertNull(option.getTokens()); + assertEquals("", option.toString()); + } + + @ParameterizedTest + @MethodSource("structuredOptions") + void tokenReplacementRejectsStructuredOptionsWithoutChangingThem(ColumnOption option) { + ColumnOption.Kind kind = option.getKind(); + String sql = option.toString(); + List snapshot = option.getTokens(); + assertReadOnly(snapshot); + assertThrows(IllegalStateException.class, () -> option.setTokens(List.of("raw"))); + assertThrows(IllegalStateException.class, () -> option.setTokens(null)); + assertEquals(kind, option.getKind()); + assertEquals(sql, option.toString()); + assertEquals(snapshot, option.getTokens()); + } + + private static Stream structuredOptions() { + return Stream.of(ColumnOption.serialDefaultValue(), + ColumnOption.reference(new ForeignKeyReference().withTable(new Table("parent")) + .withReferencedColumnNames(List.of("id"))), + ColumnOption + .identity(new IdentityDefinition(IdentityDefinition.GenerationMode.ALWAYS)), + ColumnOption.constraint(new KeyConstraint().withType("PRIMARY KEY")), + ColumnOption.constraint(new KeyConstraint().withType("UNIQUE")), + ColumnOption.defaultValue(new LongValue(1)), + ColumnOption.nullability(false), ColumnOption.nullability(true), + ColumnOption.collate("utf8mb4_bin"), + ColumnOption.comment(new StringValue("comment")), + ColumnOption.onUpdate(new LongValue(1)), + ColumnOption.generated(new GeneratedColumnDefinition(new LongValue(1))), + ColumnOption.autoIncrement(), ColumnOption.visibility(false), + ColumnOption.storage(ColumnOption.Storage.MEMORY), + ColumnOption.compression("pglz"), + ColumnOption.foreignOptions(List.of( + new ForeignDataOption("column_name", new StringValue("remote_id"))))); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id INT DEFAULT 1)", + "ALTER TABLE t ADD COLUMN id INT DEFAULT 1"}) + void parsedDefaultsKeepLiveExpressionsAndStableTokenSnapshots(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + ColumnDefinition column = column(statement); + ColumnOption option = column.getColumnOptions().get(0); + List snapshot = option.getTokens(); + assertReadOnly(snapshot); + LongValue original = (LongValue) option.getDefaultExpression(); + original.setValue(7); + assertEquals(List.of("DEFAULT", "7"), option.getTokens()); + assertEquals(List.of("DEFAULT", "1"), snapshot); + LongValue replacement = new LongValue(2); + option.setDefaultExpression(replacement); + original.setValue(99); + assertEquals(List.of("DEFAULT", "2"), option.getTokens()); + assertVisited(column, replacement); + assertCustomRendering(statement, "DEFAULT 2", "DEFAULT 42"); + assertRoundTrip(statement); + + ColumnOption raw = ColumnOption.raw(List.of("DEFAULT", "8")); + column.getColumnOptions().set(0, raw); + assertEquals(ColumnOption.Kind.OTHER, raw.getKind()); + assertNull(raw.getDefaultExpression()); + assertVisited(column); + replacement.setValue(100); + assertEquals(List.of("DEFAULT", "8"), raw.getTokens()); + assertRoundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (source INT, computed INT GENERATED ALWAYS AS (source + 1) STORED)", + "ALTER TABLE t ADD COLUMN computed INT GENERATED ALWAYS AS (source + 1) STORED"}) + void generatedDefinitionAndOptionExposeReadOnlySnapshotsWithLiveChildren(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + ColumnDefinition column = column(statement); + ColumnOption option = column.getColumnOptions().get(0); + GeneratedColumnDefinition generated = option.getGeneratedDefinition(); + List snapshot = generated.getTokens(); + List optionSnapshot = option.getTokens(); + assertReadOnly(snapshot); + assertReadOnly(optionSnapshot); + generated.setGeneratedAlways(false); + generated.setStorage(GeneratedColumnDefinition.Storage.VIRTUAL); + Addition expression = (Addition) CCJSqlParserUtil.parseExpression("source + 2"); + generated.setExpression(expression); + ((LongValue) expression.getRightExpression()).setValue(3); + assertSame(generated, option.getGeneratedDefinition()); + assertEquals(List.of("AS", "(source + 3)", "VIRTUAL"), generated.getTokens()); + assertEquals(generated.getTokens(), option.getTokens()); + assertEquals(List.of("GENERATED", "ALWAYS", "AS", "(source + 1)", "STORED"), snapshot); + assertEquals(snapshot, optionSnapshot); + assertVisited(column, expression); + assertCustomRendering(statement, "source + 3", "source + 42"); + assertRoundTrip(statement); + } + + private static void assertReadOnly(List tokens) { + assertThrows(UnsupportedOperationException.class, () -> tokens.set(0, "changed")); + assertThrows(UnsupportedOperationException.class, () -> tokens.add("changed")); + assertThrows(UnsupportedOperationException.class, tokens::clear); + } + + private static void assertVisited(ColumnDefinition column, Expression... expected) { + List expressions = new ArrayList<>(); + TableDefinitionTraversal.visit(column, expressions::add, ignored -> { + }); + assertEquals(List.of(expected), expressions); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static ColumnDefinition column(Statement statement) { + if (statement instanceof CreateTable) { + List columns = ((CreateTable) statement).getColumnDefinitions(); + return columns.get(columns.size() - 1); + } + return ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0); + } + + private static void assertCustomRendering(Statement statement, String before, String after) + throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(42); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), sql), null); + assertEquals(statement.toString().replace(before, after), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ConstraintInspectionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ConstraintInspectionTest.java new file mode 100644 index 0000000000..dd622c7517 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ConstraintInspectionTest.java @@ -0,0 +1,414 @@ +/*- + * #%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.statement.create; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterOperation; +import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey; +import net.sf.jsqlparser.statement.alter.AlterExpressionDrop; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.ConstraintDeclaration; +import net.sf.jsqlparser.statement.create.table.ConstraintUsingIndex; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.KeyColumnSource; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConstraintInspectionTest { + @Test + void combinesInlineAndTableDeclarationsInSourceOrder() throws Exception { + CreateTable table = table("CREATE TABLE child (id INT CONSTRAINT pk PRIMARY KEY, " + + "UNIQUE (id), parent_id INT REFERENCES parent(id), CHECK (id > 0), " + + "code INT NOT NULL DEFAULT 1)", Dialect.POSTGRESQL); + String before = table.toString(); + List declarations = table.getConstraints(); + assertEquals(List.of(ConstraintKind.PRIMARY_KEY, ConstraintKind.UNIQUE, + ConstraintKind.FOREIGN_KEY, ConstraintKind.CHECK, + ConstraintKind.NOT_NULL), kinds(declarations)); + assertSame(table.getColumnDefinitions().get(0), declarations.get(0).getColumn()); + assertEquals("pk", declarations.get(0).getConstraint().getName()); + assertSame(table.getTableConstraints().get(0), declarations.get(1).getConstraint()); + assertNull(declarations.get(1).getColumn()); + assertNull(declarations.get(1).getColumnOption()); + assertNull(declarations.get(2).getConstraint()); + assertEquals("parent", declarations.get(2).getColumnOption().getForeignKeyReference() + .getTable().getName()); + assertEquals(2, table.getTableConstraints().size()); + assertTrue(table.getIndexes().isEmpty()); + assertEquals(before, table.toString()); + assertRoundTrip(table, Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = {"UNIQUE", "UNIQUE KEY", "UNIQUE INDEX"}) + void includesMySqlTableUniquenessDeclarationsButNotOrdinaryIndexes(String keyword) + throws Exception { + CreateTable table = table("CREATE TABLE t (id INT, " + keyword + + " uk (id), KEY ix (id), FULLTEXT KEY ft (body), body TEXT)", Dialect.MYSQL); + assertEquals(List.of(ConstraintKind.UNIQUE), kinds(table.getConstraints())); + assertEquals(keyword, table.getConstraints().get(0).getConstraint().getType()); + assertRoundTrip(table, Dialect.MYSQL); + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "POSTGRESQL"}) + void keepsStandaloneUniqueIndexesInTheirStatementContext(Dialect dialect) throws Exception { + Statement statement = parse("CREATE UNIQUE INDEX uq ON t (id)", dialect); + CreateIndex index = assertInstanceOf(CreateIndex.class, statement); + assertEquals(Index.Kind.UNIQUE, index.getIndex().getKind()); + AlterExpression drop = action("ALTER TABLE t DROP INDEX uq", Dialect.MYSQL); + assertEquals(ConstraintKind.OTHER, drop.getConstraintKind()); + assertRoundTrip(statement, dialect); + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "POSTGRESQL"}) + void callersCanFindAndEditPrimaryKeyOwnersWithoutMovingInlineDeclarations(Dialect dialect) + throws Exception { + CreateTable table = table("CREATE TABLE t (id INT PRIMARY KEY, label TEXT)", dialect); + ConstraintDeclaration primary = table.getConstraints().stream() + .filter(declaration -> declaration.getKind() == ConstraintKind.PRIMARY_KEY) + .findFirst().get(); + primary.getColumn().setColumnName("new_id"); + assertEquals("new_id", table.getColumnDefinitions().get(0).getColumnName()); + assertTrue(table.getIndexes() == null || table.getIndexes().isEmpty()); + assertTrue(table.toString().contains("new_id INT PRIMARY KEY")); + assertRoundTrip(table, dialect); + } + + @Test + void membershipIsASnapshotWhileKindsAndOwnersRemainLive() throws Exception { + CreateTable table = table("CREATE TABLE t (id INT, UNIQUE (id))", Dialect.POSTGRESQL); + List snapshot = table.getConstraints(); + ConstraintDeclaration declaration = snapshot.get(0); + NamedConstraint index = declaration.getConstraint(); + assertThrows(UnsupportedOperationException.class, snapshot::clear); + index.setType("PRIMARY KEY"); + assertEquals(ConstraintKind.PRIMARY_KEY, declaration.getKind()); + assertRoundTrip(table, Dialect.POSTGRESQL); + String type = index.getType(); + index.setKind(ConstraintKind.OTHER); + assertEquals(type, index.getType()); + assertEquals(ConstraintKind.OTHER, declaration.getKind()); + assertEquals(1, snapshot.size()); + assertEquals(List.of(ConstraintKind.OTHER), kinds(table.getConstraints())); + assertSame(index, table.getTableConstraints().get(0)); + index.setType("UNIQUE"); + ColumnOption notNull = ColumnOption.nullability(false); + table.getColumnDefinitions().get(0).addColumnOptions(notNull); + assertEquals(1, snapshot.size()); + List refreshed = table.getConstraints(); + assertEquals(List.of(ConstraintKind.NOT_NULL, ConstraintKind.UNIQUE), kinds(refreshed)); + notNull.setNullable(true); + assertEquals(ConstraintKind.OTHER, refreshed.get(0).getKind()); + assertEquals(List.of(ConstraintKind.UNIQUE), kinds(table.getConstraints())); + assertRoundTrip(table, Dialect.POSTGRESQL); + } + + @Test + void physicalIndexAndConstraintViewsMutateIndependently() throws Exception { + CreateTable table = table("CREATE TABLE t (id INT, KEY ix (id), " + + "CONSTRAINT uq UNIQUE (id), CONSTRAINT ck CHECK (id > 0))", Dialect.MYSQL); + Index index = table.getIndexes().get(0); + NamedConstraint check = table.getTableConstraints().get(1); + table.getTableConstraints().remove(0); + assertSame(index, table.getTableElements().get(1)); + assertSame(check, table.getTableConstraints().get(0)); + assertEquals(List.of(ConstraintKind.CHECK), kinds(table.getConstraints())); + KeyConstraint primary = new KeyConstraint().withType("PRIMARY KEY") + .withColumnsNames(List.of("id")); + table.getTableConstraints().add(0, primary); + table.setIndexes(table.getIndexes()); + assertEquals(List.of(ConstraintKind.PRIMARY_KEY, ConstraintKind.CHECK), + kinds(table.getConstraints())); + assertSame(index, table.getTableElements().get(1)); + assertSame(primary, table.getTableElements().get(2)); + assertRoundTrip(table, Dialect.MYSQL); + table.getIndexes().clear(); + assertEquals(2, table.getTableConstraints().size()); + assertRoundTrip(table, Dialect.MYSQL); + } + + @Test + void inlineStructuredConstraintsRemainVisibleWithUnknownKind() throws Exception { + CreateTable table = table("CREATE TABLE t (id INT PRIMARY KEY)", Dialect.POSTGRESQL); + NamedConstraint primary = table.getConstraints().get(0).getConstraint(); + primary.setKind(ConstraintKind.OTHER); + assertEquals(List.of(ConstraintKind.OTHER), kinds(table.getConstraints())); + assertSame(primary, table.getConstraints().get(0).getConstraint()); + assertRoundTrip(table, Dialect.POSTGRESQL); + } + + @Test + void legacyConstructionHasDocumentedOrderingAndDoesNotInterpretRawTokens() { + CreateTable table = new CreateTable(); + ColumnDefinition column = new ColumnDefinition("id", new ColDataType("INT")); + column.setColumnOptions(List.of(ColumnOption.raw("PRIMARY", "KEY"), + ColumnOption.nullability(false), ColumnOption.nullability(true))); + table.setColumnDefinitions(List.of(column)); + KeyConstraint primary = + new KeyConstraint().withType("PRIMARY KEY").withColumnsNames(List.of("id")); + table.setIndexes(List.of(new Index().withType("SPATIAL INDEX"))); + table.setTableConstraints(List.of(primary)); + assertEquals(List.of(ConstraintKind.NOT_NULL, ConstraintKind.PRIMARY_KEY), + kinds(table.getConstraints())); + assertSame(column, table.getConstraints().get(0).getColumn()); + assertSame(primary, table.getConstraints().get(1).getConstraint()); + assertTrue(!Index.class.isAssignableFrom(KeyConstraint.class)); + assertTrue(!Index.class.isAssignableFrom(NamedConstraint.class)); + assertEquals(ConstraintKind.OTHER, ColumnOption.constraint(null).getConstraintKind()); + } + + static Stream alterConstraints() { + return Stream.of( + Arguments.of("ADD PRIMARY KEY (id)", ConstraintKind.PRIMARY_KEY, + Dialect.POSTGRESQL), + Arguments.of("ADD UNIQUE (id)", ConstraintKind.UNIQUE, Dialect.POSTGRESQL), + Arguments.of("ADD UNIQUE KEY uq (id)", ConstraintKind.UNIQUE, Dialect.MYSQL), + Arguments.of("ADD FOREIGN KEY (id) REFERENCES parent(id)", + ConstraintKind.FOREIGN_KEY, + Dialect.POSTGRESQL), + Arguments.of("ADD CHECK (id > 0)", ConstraintKind.CHECK, Dialect.POSTGRESQL), + Arguments.of("ADD CONSTRAINT pk PRIMARY KEY USING INDEX ix", + ConstraintKind.PRIMARY_KEY, + Dialect.POSTGRESQL), + Arguments.of("ADD CONSTRAINT ex EXCLUDE USING gist (id WITH =)", + ConstraintKind.EXCLUDE, + Dialect.POSTGRESQL), + Arguments.of("DROP PRIMARY KEY", ConstraintKind.PRIMARY_KEY, Dialect.MYSQL), + Arguments.of("DROP FOREIGN KEY fk", ConstraintKind.FOREIGN_KEY, Dialect.MYSQL), + Arguments.of("DROP CHECK ck", ConstraintKind.CHECK, Dialect.MYSQL), + Arguments.of("DROP CONSTRAINT pk", ConstraintKind.OTHER, Dialect.POSTGRESQL), + Arguments.of("ALTER CONSTRAINT pk DEFERRABLE", ConstraintKind.OTHER, + Dialect.POSTGRESQL), + Arguments.of("ALTER CHECK ck NOT ENFORCED", ConstraintKind.CHECK, Dialect.MYSQL), + Arguments.of("ALTER CONSTRAINT ck NOT ENFORCED", ConstraintKind.OTHER, + Dialect.MYSQL), + Arguments.of("ADD INDEX ix (id)", ConstraintKind.OTHER, Dialect.MYSQL), + Arguments.of("DROP INDEX ix", ConstraintKind.OTHER, Dialect.MYSQL), + Arguments.of("DROP COLUMN id", ConstraintKind.OTHER, Dialect.POSTGRESQL), + Arguments.of("ADD COLUMN id INT PRIMARY KEY", ConstraintKind.OTHER, + Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @MethodSource("alterConstraints") + void classifiesOnlyTheExplicitKindOfTheActiveAction(String sql, ConstraintKind expected, + Dialect dialect) + throws Exception { + Alter alter = (Alter) parse("ALTER TABLE t " + sql, dialect); + String before = alter.toString(); + assertEquals(expected, alter.getAlterExpressions().get(0).getConstraintKind()); + assertEquals(before, alter.toString()); + assertRoundTrip(alter, dialect); + } + + @Test + void changingOperationDoesNotGuessANamedDropFromAnOldDefinition() throws Exception { + AlterExpression action = action("ALTER TABLE t ADD PRIMARY KEY (id)", Dialect.POSTGRESQL); + assertEquals(ConstraintKind.PRIMARY_KEY, action.getConstraintKind()); + action.setOperation(AlterOperation.DROP); + action.setConstraintName("unrelated_constraint"); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + action.setOperation(AlterOperation.DROP_FOREIGN_KEY); + assertEquals(ConstraintKind.FOREIGN_KEY, action.getConstraintKind()); + action.setOperation(AlterOperation.ADD); + action.setConstraintName(null); + action.getConstraint().setType("UNIQUE"); + assertEquals(ConstraintKind.UNIQUE, action.getConstraintKind()); + } + + @Test + void activeColumnAndCommentPayloadsHideStaleIndexMetadata() throws Exception { + List> edits = List.of( + action -> action.addColSetNotNull(new AlterExpression.ColumnSetNotNull("id")), + action -> action.addColDropNotNull(new AlterExpression.ColumnDropNotNull("id")), + action -> action.addColDropDefault(new AlterExpression.ColumnDropDefault("id")), + action -> action.setCommentText("'description'")); + for (Consumer edit : edits) { + AlterExpression action = action("ALTER TABLE t ADD PRIMARY KEY (id)", + Dialect.POSTGRESQL); + edit.accept(action); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + assertTrue(!action.toString().contains("PRIMARY KEY")); + } + } + + static Stream specializedActions() { + return Stream.of( + Arguments.of("ALTER TABLE t ALTER CONSTRAINT ck DEFERRABLE", Dialect.POSTGRESQL), + Arguments.of("ALTER TABLE t OWNER TO alice", Dialect.POSTGRESQL), + Arguments.of("ALTER FOREIGN TABLE t OPTIONS (ADD filename 'file')", + Dialect.POSTGRESQL), + Arguments.of("ALTER TABLE t ORDER BY id", Dialect.MYSQL), + Arguments.of("ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4", Dialect.MYSQL), + Arguments.of("ALTER TABLE t DETACH PARTITION p", Dialect.POSTGRESQL), + Arguments.of("ALTER TABLE t ENGINE = InnoDB", Dialect.MYSQL), + Arguments.of("ALTER TABLE t RENAME COLUMN id TO new_id", Dialect.MYSQL)); + } + + @ParameterizedTest + @MethodSource("specializedActions") + void specializedActionsDoNotUseUnrelatedInheritedConstraintFields(String sql, Dialect dialect) + throws Exception { + Alter statement = (Alter) parse(sql, dialect); + AlterExpression action = statement.getAlterExpressions().get(0); + assertRoundTrip(statement, dialect); + action.setConstraint( + new KeyConstraint().withType("PRIMARY KEY").withColumnsNames(List.of("id"))); + action.setConstraintType("CHECK"); + action.setConstraintSymbol("unrelated"); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + action.setOperation(AlterOperation.DROP_FOREIGN_KEY); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + action.setOperation(null); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + } + + @Test + void fixedPrimaryKeyActionKeepsItsKindWhenInheritedMetadataChanges() throws Exception { + AlterExpressionPrimaryKey action = assertInstanceOf(AlterExpressionPrimaryKey.class, + action("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id)", Dialect.COCKROACHDB)); + String before = action.toString(); + action.setOperation(AlterOperation.DROP_FOREIGN_KEY); + action.getConstraint().setKind(ConstraintKind.UNIQUE); + assertEquals(ConstraintKind.PRIMARY_KEY, action.getConstraintKind()); + assertEquals(before, action.toString()); + action.setOperation(null); + assertEquals(ConstraintKind.PRIMARY_KEY, action.getConstraintKind()); + assertEquals(before, action.toString()); + } + + @Test + void dropColumnActionDoesNotBecomeAPrimaryKeyDeclarationFromStaleFields() throws Exception { + Alter statement = (Alter) parse("ALTER TABLE t DROP COLUMN id", Dialect.POSTGRESQL); + AlterExpressionDrop action = assertInstanceOf(AlterExpressionDrop.class, + statement.getAlterExpressions().get(0)); + String before = action.toString(); + action.setConstraint( + new KeyConstraint().withType("PRIMARY KEY").withColumnsNames(List.of("id"))); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + assertEquals(before, action.toString()); + assertRoundTrip(statement, Dialect.POSTGRESQL); + + // The shared renderer follows an explicitly changed operation; the active payload is + // still a column action rather than the stale PRIMARY KEY definition. + action.setOperation(AlterOperation.ADD); + assertEquals(ConstraintKind.OTHER, action.getConstraintKind()); + assertEquals("ADD COLUMN id", action.toString()); + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals("ALTER TABLE t ADD COLUMN id", sql.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP PRIMARY KEY", "DROP FOREIGN KEY fk", "DROP CHECK ck"}) + void distinguishesBaseRenamePrecedenceFromSpecializedDropRendering(String sql) + throws Exception { + AlterExpression drop = action("ALTER TABLE t " + sql, Dialect.MYSQL); + ConstraintKind expected = drop.getConstraintKind(); + String before = drop.toString(); + Index oldName = new Index().withName("old_name"); + Index newName = new Index().withName("new_name"); + drop.setOldIndex(oldName); + drop.setIndex(newName); + assertEquals(expected, drop.getConstraintKind()); + assertEquals(before, drop.toString()); + + AlterExpression base = new AlterExpression().withOperation(drop.getOperation()); + base.setOldIndex(oldName); + base.setIndex(newName); + assertTrue(base.toString().startsWith("RENAME")); + assertEquals(ConstraintKind.OTHER, base.getConstraintKind()); + + base.setOperation(AlterOperation.ALTER); + base.setConstraintType("CHECK"); + base.setConstraintSymbol("ck"); + assertTrue(base.toString().startsWith("ALTER CHECK ck")); + assertEquals(ConstraintKind.CHECK, base.getConstraintKind()); + } + + @Test + void supportsLegacyOnlyKeysWithoutMistakingDropColumnsForPrimaryKeys() { + AlterExpression legacy = new AlterExpression().withOperation(AlterOperation.ADD) + .withPkColumns(new ArrayList<>(List.of("id"))); + assertEquals(ConstraintKind.PRIMARY_KEY, legacy.getConstraintKind()); + legacy.setOperation(AlterOperation.DROP); + assertEquals(ConstraintKind.OTHER, legacy.getConstraintKind()); + legacy.setOperation(AlterOperation.DROP_UNIQUE); + assertEquals(ConstraintKind.UNIQUE, legacy.getConstraintKind()); + assertEquals(ConstraintKind.OTHER, new AlterExpression().getConstraintKind()); + } + + @Test + void existingIndexConstraintsRetainTheirEditableSourceAndHaveNoInventedKeys() throws Exception { + AlterExpression action = + action("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY USING INDEX ix", + Dialect.POSTGRESQL); + ConstraintUsingIndex constraint = assertInstanceOf(ConstraintUsingIndex.class, + action.getConstraint()); + assertEquals(ConstraintKind.PRIMARY_KEY, action.getConstraintKind()); + assertTrue(!KeyColumnSource.class.isAssignableFrom(ConstraintUsingIndex.class)); + constraint.setExistingIndexName("new_ix"); + assertTrue(action.toString().contains("USING INDEX new_ix")); + } + + private static List kinds(List declarations) { + return declarations.stream().map(ConstraintDeclaration::getKind) + .collect(Collectors.toList()); + } + + private static Statement parse(String sql, Dialect dialect) throws Exception { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static CreateTable table(String sql, Dialect dialect) throws Exception { + return (CreateTable) parse(sql, dialect); + } + + private static AlterExpression action(String sql, Dialect dialect) throws Exception { + return ((Alter) parse(sql, dialect)).getAlterExpressions().get(0); + } + + private static void assertRoundTrip(Statement statement, Dialect dialect) throws Exception { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java index 66880938c2..52f250afeb 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java @@ -15,8 +15,18 @@ import java.util.Arrays; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.MySqlDefiner; +import net.sf.jsqlparser.statement.CreateFunctionalStatement; import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.event.CreateEvent; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import net.sf.jsqlparser.statement.create.function.CreateFunction; import net.sf.jsqlparser.statement.create.procedure.CreateProcedure; import org.junit.jupiter.api.Test; @@ -89,6 +99,65 @@ public void createOrReplaceFunctionMinimal() throws JSQLParserException { assertDeparse(func, statement); } + @ParameterizedTest + @ValueSource(strings = { + "CREATE DEFINER = CURRENT_USER FUNCTION f2 (s CHAR(20)) RETURNS CHAR(50) " + + "CHARSET utf8mb4 NO SQL RETURN CONCAT('Hello, ', s, '!')", + "CREATE DEFINER = CURRENT_USER PROCEDURE p3 () SELECT 1", + "CREATE DEFINER = `root`@`%` FUNCTION f3 () RETURNS INT RETURN 1", + "CREATE DEFINER = 'admin'@'localhost' PROCEDURE p (IN a INT) BEGIN SELECT a; END", + "CREATE OR REPLACE DEFINER = CURRENT_USER() PROCEDURE p () SELECT 1"}) + public void createRoutineWithMySqlDefiner(String sql) throws JSQLParserException { + for (Dialect dialect : new Dialect[] {Dialect.MYSQL, Dialect.MARIADB, null}) { + CreateFunctionalStatement statement = + (CreateFunctionalStatement) assertSqlCanBeParsedAndDeparsed( + sql, true, dialect == null ? parser -> { + } : parser -> parser.withDialect(dialect)); + assertThat(statement.getDefiner()).isNotNull(); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertThat(deparsed.toString()).isEqualTo(statement.toString()); + assertThat(CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1").getStatements()) + .hasSize(2); + } + } + + @Test + public void mySqlDefinerAccount() throws JSQLParserException { + CreateFunction function = (CreateFunction) CCJSqlParserUtil.parse( + "CREATE DEFINER='root'@'%' FUNCTION f3 () RETURNS INT RETURN 1"); + MySqlDefiner definer = function.getDefiner(); + assertThat(definer.getUser().getValue()).isEqualTo("root"); + assertThat(definer.getHost().getValue()).isEqualTo("%"); + assertThat(function.getFunctionDeclarationParts()).startsWith("f3", "(", ")"); + + CreateProcedure procedure = (CreateProcedure) CCJSqlParserUtil + .parse("CREATE DEFINER = CURRENT_USER PROCEDURE p3 () SELECT 1"); + assertThat(procedure.getDefiner().getUser().getValue()).isEqualTo("CURRENT_USER"); + assertThat(procedure.getDefiner().getHost()).isNull(); + + procedure.setDefiner(new MySqlDefiner().withUser(new StringValue("app")) + .withHost(new StringValue("%"))); + assertThat(procedure.toString()) + .isEqualTo("CREATE DEFINER = 'app'@'%' PROCEDURE p3 ( ) SELECT 1"); + procedure.setDefiner(null); + assertThat(procedure.toString()).isEqualTo("CREATE PROCEDURE p3 ( ) SELECT 1"); + } + + @Test + public void definerDispatchKeepsTriggersAndViews() throws JSQLParserException { + assertThat(CCJSqlParserUtil.parse("CREATE DEFINER = CURRENT_USER TRIGGER t " + + "BEFORE INSERT ON t1 FOR EACH ROW SET NEW.a = 1")) + .isInstanceOf(CreateTrigger.class); + assertThat(CCJSqlParserUtil.parse("CREATE DEFINER = CURRENT_USER VIEW v AS SELECT 1")) + .isInstanceOf(CreateView.class); + assertThat(CCJSqlParserUtil.parse("CREATE FUNCTION f() RETURNS int AS 'SELECT 1'")) + .isInstanceOf(CreateFunction.class) + .extracting(statement -> ((CreateFunction) statement).getDefiner()).isNull(); + assertThat(CCJSqlParserUtil.parse("CREATE DEFINER = x EVENT e " + + "ON SCHEDULE EVERY 1 DAY DO SELECT 1")).isInstanceOf(CreateEvent.class); + } + @Test public void createFunctionWithPositionalParametersAcrossStatementsIssue2322() throws JSQLParserException { diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexOptionStateTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexOptionStateTest.java new file mode 100644 index 0000000000..981b68f210 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexOptionStateTest.java @@ -0,0 +1,240 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; + +class CreateIndexOptionStateTest { + private static final String SQL = "CREATE UNIQUE INDEX ix ON t (id) INCLUDE (payload) " + + "NULLS NOT DISTINCT WITH (fillfactor = 80) TABLESPACE fast_space"; + + @Test + void parsedOptionsAreSharedAndIndexEditsReachBothRenderers() throws Exception { + CreateIndex statement = parse(SQL); + Index index = statement.getIndex(); + assertSame(statement.getIncludeColumns(), index.getIncludeColumns()); + assertSame(statement.getStorageParameters(), index.getStorageParameters()); + assertEquals(Boolean.FALSE, index.getNullsDistinct()); + assertEquals("fast_space", index.getTableSpace()); + + index.getIncludeColumns().add("extra"); + index.setNullsDistinct(true); + index.getStorageParameters().get(0).setValue(new LongValue(90)); + index.setTableSpace("other_space"); + + assertEquals(List.of("payload", "extra"), statement.getIncludeColumns()); + assertEquals(Boolean.TRUE, statement.getNullsDistinct()); + assertEquals("other_space", statement.getTableSpace()); + assertRoundTrip("CREATE UNIQUE INDEX ix ON t (id) INCLUDE (payload, extra) " + + "NULLS DISTINCT WITH (fillfactor = 90) TABLESPACE other_space", statement); + } + + @Test + void statementSettersAndClearsUpdateTheIndexDefinition() throws Exception { + CreateIndex statement = parse("CREATE UNIQUE INDEX ix ON t (id)"); + statement.setIncludeColumns(List.of("payload")); + statement.setNullsDistinct(false); + statement.setStorageParameters(List.of(option(80))); + statement.setTableSpace("fast_space"); + Index index = statement.getIndex(); + assertSame(statement.getIncludeColumns(), index.getIncludeColumns()); + assertSame(statement.getStorageParameters(), index.getStorageParameters()); + assertEquals(Boolean.FALSE, index.getNullsDistinct()); + assertEquals("fast_space", index.getTableSpace()); + assertRoundTrip(SQL, statement); + + statement.setIncludeColumns(null); + statement.setNullsDistinct(null); + statement.setStorageParameters(null); + statement.setTableSpace(null); + assertNull(index.getIncludeColumns()); + assertNull(index.getNullsDistinct()); + assertNull(index.getStorageParameters()); + assertNull(index.getTableSpace()); + assertRoundTrip("CREATE UNIQUE INDEX ix ON t (id)", statement); + } + + @Test + void optionsCanBeConfiguredBeforeTheIndexWithoutLosingSuppliedOptions() throws Exception { + CreateIndex statement = new CreateIndex().withTable(new Table("t")) + .withIncludeColumns(List.of("payload")) + .withNullsDistinct(false) + .withStorageParameters(List.of(option(80))) + .withTableSpace("staged_space"); + assertNull(statement.getIndex()); + assertEquals("staged_space", statement.getTableSpace()); + + Index index = new Index().withType("UNIQUE").withName("ix") + .withColumnsNames(List.of("id")); + index.setTableSpace("fast_space"); + statement.setIndex(index); + + assertSame(index, statement.getIndex()); + assertSame(statement.getStorageParameters(), index.getStorageParameters()); + assertRoundTrip(SQL, statement); + } + + @Test + void replacementOptionsTakePrecedenceAndMissingOptionsSurviveDetachment() throws Exception { + CreateIndex statement = parse(SQL); + Index replacement = new Index().withType("UNIQUE").withName("replacement") + .withColumnsNames(List.of("id")); + replacement.setIncludeColumns(List.of("extra")); + replacement.setStorageParameters(List.of(option(90))); + statement.setIndex(replacement); + statement.setIndex(replacement); + assertSame(replacement.getIncludeColumns(), statement.getIncludeColumns()); + assertRoundTrip("CREATE UNIQUE INDEX replacement ON t (id) INCLUDE (extra) " + + "NULLS NOT DISTINCT WITH (fillfactor = 90) TABLESPACE fast_space", statement); + + statement.setIndex(null); + statement.setIndex(null); + assertNull(statement.getIndex()); + assertEquals(List.of("extra"), statement.getIncludeColumns()); + assertEquals(Boolean.FALSE, statement.getNullsDistinct()); + assertEquals("fast_space", statement.getTableSpace()); + statement.setTableSpace(null); + Index reattached = new Index().withType("UNIQUE").withName("reattached") + .withColumnsNames(List.of("id")); + statement.setIndex(reattached); + assertSame(reattached.getStorageParameters(), statement.getStorageParameters()); + assertNull(reattached.getTableSpace()); + assertRoundTrip("CREATE UNIQUE INDEX reattached ON t (id) INCLUDE (extra) " + + "NULLS NOT DISTINCT WITH (fillfactor = 90)", statement); + } + + @Test + void detachedOptionSettersDoNotMutateTheRemovedIndex() throws Exception { + CreateIndex statement = parse(SQL); + Index removed = statement.getIndex(); + statement.setIndex(null); + + assertNotSame(removed.getIncludeColumns(), statement.getIncludeColumns()); + assertNotSame(removed.getStorageParameters(), statement.getStorageParameters()); + assertSame(removed.getStorageParameters().get(0), statement.getStorageParameters().get(0)); + List detachedColumns = statement.getIncludeColumns(); + List detachedParameters = statement.getStorageParameters(); + statement.setIndex(null); + assertSame(detachedColumns, statement.getIncludeColumns()); + assertSame(detachedParameters, statement.getStorageParameters()); + statement.getIncludeColumns().add("extra"); + assertEquals(List.of("payload", "extra"), statement.getIncludeColumns()); + assertEquals(List.of("payload"), removed.getIncludeColumns()); + + statement.setTableSpace("detached_space"); + statement.setNullsDistinct(true); + statement.setIncludeColumns(List.of("detached_payload")); + statement.setStorageParameters(List.of(option(90))); + assertEquals("fast_space", removed.getTableSpace()); + assertEquals(Boolean.FALSE, removed.getNullsDistinct()); + assertEquals(List.of("payload"), removed.getIncludeColumns()); + assertEquals("80", removed.getStorageParameters().get(0).getValue().toString()); + + removed.setTableSpace("external_space"); + removed.setNullsDistinct(null); + assertEquals("detached_space", statement.getTableSpace()); + assertEquals(Boolean.TRUE, statement.getNullsDistinct()); + statement.setIndex(new Index().withType("UNIQUE").withName("reattached") + .withColumnsNames(List.of("id"))); + assertRoundTrip("CREATE UNIQUE INDEX reattached ON t (id) INCLUDE (detached_payload) " + + "NULLS DISTINCT WITH (fillfactor = 90) TABLESPACE detached_space", statement); + } + + @Test + void optionsAreVisitedBeforeAttachmentAndAfterDetachment() throws Exception { + for (CreateIndex statement : List.of(new CreateIndex(), parse(SQL))) { + statement.setIndex(null); + statement.setStorageParameters(List.of(option(80))); + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }; + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "context"); + assertEquals(List.of(80L), values); + } + } + + @Test + void indexOptionEditsAreVisitedOnceAndCanBeDeparsedThroughCustomVisitors() throws Exception { + CreateIndex statement = parse(SQL); + statement.getIndex().setStorageParameters(List.of(option(90))); + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }; + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "context"); + assertEquals(List.of(90L), values); + + StringBuilder output = new StringBuilder(); + ExpressionDeParser deparser = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 1); + } + }; + statement.accept(new StatementDeParser(deparser, new SelectDeParser(), output)); + assertEquals(SQL.replace("80", "91"), output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + assertEquals(SQL.replace("80", "90"), statement.toString()); + } + + private static IndexOption option(long value) { + return new IndexOption("fillfactor", new LongValue(value), true); + } + + private static CreateIndex parse(String sql) throws Exception { + return (CreateIndex) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(String expected, CreateIndex statement) throws Exception { + assertEquals(expected, statement.toString()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(expected, output.toString()); + CreateIndex reparsed = parse(expected); + assertEquals(expected, reparsed.toString()); + assertEquals(statement.getIncludeColumns(), reparsed.getIndex().getIncludeColumns()); + assertEquals(statement.getNullsDistinct(), reparsed.getIndex().getNullsDistinct()); + assertEquals(statement.getTableSpace(), reparsed.getIndex().getTableSpace()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java index 2096b97301..7fa9d1bcc5 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement.create; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.KeyElement; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -19,6 +21,7 @@ import java.util.List; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserManager; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.statement.create.index.CreateIndex; import org.junit.jupiter.api.Test; @@ -174,4 +177,123 @@ public void testCreateIndexVisibility() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("CREATE INDEX idx_a ON t1 (a) INVISIBLE", true); assertSqlCanBeParsedAndDeparsed("CREATE INDEX idx_a ON t1 (a) VISIBLE", true); } + + @Test + public void testCreateIndexIncludeIssue2459() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE INDEX idx_a ON t1 (a) INCLUDE (b, c)"); + } + + @Test + public void testCreateIndexKeyPartWithPrefixLengthAndDirectionIssue2490() + throws JSQLParserException { + // MySQL writes the prefix length without a space, JSqlParser deparses it with one. + String statement = "CREATE INDEX i03 ON t (c1(20) DESC)"; + CreateIndex createIndex = (CreateIndex) parserManager.parse(new StringReader(statement)); + + List params = createIndex.getIndex().getColumns().get(0).getParams(); + assertEquals(2, params.size()); + assertEquals("(20)", params.get(0)); + assertEquals("DESC", params.get(1)); + assertEquals(KeyElement.SortOrder.DESC, + createIndex.getIndex().getColumns().get(0).getSortOrder()); + + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i03 ON t (c1 (20) DESC)"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i04 ON t (c1 (20) ASC, c2 (10) DESC)"); + assertSqlCanBeParsedAndDeparsed("CREATE UNIQUE INDEX i25 ON t (c1 (10) DESC)"); + } + + @Test + public void testCreateIndexKeyBlockSizeIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i08 ON t (c1) KEY_BLOCK_SIZE = 8"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i09 ON t (c1) KEY_BLOCK_SIZE 8"); + assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX i14 ON t (c1) USING BTREE KEY_BLOCK_SIZE = 8 COMMENT 'combo' INVISIBLE"); + } + + @Test + public void testCreateIndexAlgorithmAndLockOptionsIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX i10 ON t (c1) ALGORITHM = INPLACE LOCK = NONE"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i11 ON t (c1) ALGORITHM INPLACE LOCK NONE"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i12 ON t (c1) ALGORITHM = INPLACE"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i13 ON t (c1) LOCK = NONE"); + + CreateIndex createIndex = (CreateIndex) parserManager + .parse(new StringReader("CREATE INDEX i10 ON t (c1) ALGORITHM=INPLACE LOCK=NONE")); + assertEquals(List.of("ALGORITHM", "=", "INPLACE", "LOCK", "=", "NONE"), + createIndex.getTailParameters()); + } + + @Test + public void testCreateFullTextAndSpatialIndexIssue2490() throws JSQLParserException { + // These used to fall back to UnsupportedStatement instead of producing a CreateIndex. + CreateIndex fullText = (CreateIndex) parserManager + .parse(new StringReader("CREATE FULLTEXT INDEX i17 ON t (body)")); + assertEquals("FULLTEXT", fullText.getIndex().getType()); + + CreateIndex spatial = (CreateIndex) parserManager + .parse(new StringReader("CREATE SPATIAL INDEX i19 ON t (g)")); + assertEquals("SPATIAL", spatial.getIndex().getType()); + + assertSqlCanBeParsedAndDeparsed("CREATE FULLTEXT INDEX i17 ON t (body)"); + assertSqlCanBeParsedAndDeparsed("CREATE FULLTEXT INDEX i18 ON t (body) WITH PARSER ngram"); + assertSqlCanBeParsedAndDeparsed("CREATE SPATIAL INDEX i19 ON t (g)"); + } + + @Test + public void testCreateMultiValuedIndexIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX i20 ON t ((CAST(data -> '$.zips' AS UNSIGNED ARRAY)))"); + } + + @Test + public void testPostgreSqlIndexKeyAttributesIssue2525() throws JSQLParserException { + String orderSql = "CREATE INDEX pg_idx_sort ON pg_index_test " + + "(id DESC NULLS FIRST)"; + CreateIndex ordered = (CreateIndex) assertSqlCanBeParsedAndDeparsed( + orderSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + KeyElement orderedKey = ordered.getIndex().getColumns().get(0); + assertEquals(KeyElement.SortOrder.DESC, orderedKey.getSortOrder()); + assertEquals(KeyElement.NullOrdering.FIRST, orderedKey.getNullOrdering()); + + String operatorClassSql = "CREATE INDEX pg_idx_collate ON pg_index_test " + + "(email COLLATE \"C\" text_pattern_ops)"; + CreateIndex operatorClassIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed( + operatorClassSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + KeyElement operatorClassKey = + operatorClassIndex.getIndex().getColumns().get(0); + assertEquals("\"C\"", operatorClassKey.getCollation()); + assertEquals("text_pattern_ops", operatorClassKey.getOperatorClass()); + + String optionSql = "CREATE INDEX pg_idx_opclass ON pg_index_test " + + "(email text_pattern_ops (example = 1))"; + CreateIndex optionIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed( + optionSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + IndexOption option = optionIndex.getIndex().getColumns().get(0) + .getOperatorClassParameters().get(0); + assertEquals("example", option.getName()); + assertEquals("1", option.getValue().toString()); + assertTrue(option.isUseEquals()); + } + + @Test + public void testPostgreSqlIndexTrailingClausesIssue2525() throws JSQLParserException { + String includeSql = "CREATE UNIQUE INDEX pg_idx_nulls ON pg_index_test (email) " + + "INCLUDE (payload) NULLS NOT DISTINCT"; + CreateIndex include = (CreateIndex) assertSqlCanBeParsedAndDeparsed( + includeSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(List.of("payload"), include.getIncludeColumns()); + assertEquals(Boolean.FALSE, include.getNullsDistinct()); + + String trailingSql = "CREATE INDEX pg_idx_with ON pg_index_test (id) " + + "WITH (fillfactor = 80) TABLESPACE pg_default WHERE active"; + CreateIndex trailing = (CreateIndex) assertSqlCanBeParsedAndDeparsed( + trailingSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + IndexOption fillfactor = trailing.getStorageParameters().get(0); + assertEquals("fillfactor", fillfactor.getName()); + assertEquals("80", fillfactor.getValue().toString()); + assertTrue(fillfactor.isUseEquals()); + assertEquals("pg_default", trailing.getTableSpace()); + assertEquals("active", trailing.getWhere().toString()); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTraversalTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTraversalTest.java new file mode 100644 index 0000000000..5c9f640265 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTraversalTest.java @@ -0,0 +1,72 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; + +class CreateIndexTraversalTest { + private static final String SQL = "CREATE INDEX ix ON public.t " + + "((id + 1) public.test_ops (option = 2)) WITH (fillfactor = 80) WHERE id > 3"; + + @Test + void visitsKeyOptionStorageAndPredicateExpressionsOnceWithContext() throws Exception { + CreateIndex statement = (CreateIndex) CCJSqlParserUtil.parse(SQL, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }; + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "context"); + assertEquals(List.of(1L, 2L, 80L, 3L), values); + assertEquals(Set.of("public.t"), new TablesNamesFinder().getTables(statement)); + } + + @Test + void deparsesAllExpressionsThroughTheCustomVisitor() throws Exception { + CreateIndex statement = (CreateIndex) CCJSqlParserUtil.parse(SQL, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 10); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output)); + assertEquals("CREATE INDEX ix ON public.t " + + "((id + 11) public.test_ops (option = 12)) WITH (fillfactor = 90) WHERE id > 13", + output.toString()); + assertEquals(SQL, statement.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString(), + parser -> parser.withDialect(Dialect.POSTGRESQL)).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java index 829efd2c7d..90fed32e17 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java @@ -13,6 +13,8 @@ import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; +import net.sf.jsqlparser.statement.create.policy.PolicyCommand; +import net.sf.jsqlparser.statement.create.policy.PolicyMode; import org.junit.jupiter.api.Test; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; @@ -33,6 +35,10 @@ public void testCreatePolicyBasic() throws JSQLParserException { CreatePolicy policy = (CreatePolicy) stmt; assertEquals("policy_name", policy.getPolicyName()); assertEquals("table_name", policy.getTable().getName()); + assertNull(policy.getPolicyMode()); + assertEquals(PolicyMode.PERMISSIVE, policy.getEffectivePolicyMode()); + assertNull(policy.getPolicyCommand()); + assertEquals(PolicyCommand.ALL, policy.getEffectivePolicyCommand()); } @Test @@ -55,6 +61,7 @@ public void testCreatePolicyWithForClause() throws JSQLParserException { CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql); assertEquals("SELECT", policy.getCommand()); + assertEquals(PolicyCommand.SELECT, policy.getPolicyCommand()); } @Test @@ -65,9 +72,40 @@ public void testCreatePolicyWithAllCommands() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sql, true); CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql); assertEquals(cmd, policy.getCommand()); + assertEquals(PolicyCommand.from(cmd), policy.getPolicyCommand()); } } + @Test + public void testCreateRestrictivePolicy() throws JSQLParserException { + String sql = "CREATE POLICY tenant_policy ON users AS RESTRICTIVE FOR SELECT TO app " + + "USING (tenant_id = current_user)"; + assertSqlCanBeParsedAndDeparsed(sql, true); + + CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql); + assertEquals(PolicyMode.RESTRICTIVE, policy.getPolicyMode()); + assertEquals(PolicyMode.RESTRICTIVE, policy.getEffectivePolicyMode()); + assertEquals(PolicyCommand.SELECT, policy.getPolicyCommand()); + assertEquals("app", policy.getRoles().get(0)); + assertNotNull(policy.getUsingExpression()); + } + + @Test + public void testCreateExplicitPermissivePolicy() throws JSQLParserException { + String sql = "CREATE POLICY tenant_policy ON users AS PERMISSIVE"; + assertSqlCanBeParsedAndDeparsed(sql, true); + + CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql); + assertEquals(PolicyMode.PERMISSIVE, policy.getPolicyMode()); + assertEquals(PolicyMode.PERMISSIVE, policy.getEffectivePolicyMode()); + } + + @Test + public void testCreatePolicyRejectsUnknownMode() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("CREATE POLICY p ON t AS UNKNOWN")); + } + @Test public void testCreatePolicyWithSingleRole() throws JSQLParserException { String sql = "CREATE POLICY policy1 ON table1 TO role1"; diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableDataPopulationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableDataPopulationTest.java new file mode 100644 index 0000000000..8e52932cbd --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableDataPopulationTest.java @@ -0,0 +1,86 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class CreateTableDataPopulationTest { + @ParameterizedTest + @ValueSource(strings = {"SELECT id FROM source", "TABLE source", "VALUES (1), (2)", + "(SELECT id FROM source)", "WITH c AS (SELECT 1 AS id) SELECT id FROM c", + "SELECT id FROM source UNION ALL SELECT id FROM other ORDER BY id LIMIT 4"}) + void preservesDataChoiceForQuerySources(String query) throws JSQLParserException { + for (String suffix : new String[] {"", " WITH DATA", " WITH NO DATA"}) { + CreateTable table = parse("CREATE TABLE t AS " + query + suffix); + assertEquals(suffix.isEmpty() ? null : !suffix.contains("NO"), table.getWithData()); + roundTrip(table); + } + } + + @Test + void sharesOutputAndKeepsCustomQueryVisitor() throws JSQLParserException { + CreateTable table = + parse("CREATE TEMP TABLE t (id) ON COMMIT DROP AS SELECT 7 WITH NO DATA"); + table.setWithData(true); + assertTrue(table.toString().endsWith("WITH DATA")); + ExpressionDeParser expression = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + StringBuilder out = new StringBuilder(); + table.accept(new StatementDeParser(expression, new SelectDeParser(), out)); + assertTrue(out.toString().endsWith("SELECT 107 WITH DATA")); + assertTrue(table.toString().contains("SELECT 7")); + table.setWithData(null); + assertFalse(table.toString().contains("WITH DATA")); + roundTrip(table); + } + + @Test + void retainsMaterializedViewTailAndStatementBoundaries() throws JSQLParserException { + CreateView view = (CreateView) CCJSqlParserUtil.parse( + "CREATE MATERIALIZED VIEW v AS SELECT 1 WITH NO DATA"); + assertEquals(Boolean.FALSE, view.getWithData()); + assertEquals(2, CCJSqlParserUtil.parseStatements( + "CREATE TABLE t AS SELECT 1 WITH NO DATA; SELECT 2").size()); + for (String sql : List.of("CREATE TABLE t AS SELECT 1 WITH NO", + "CREATE TABLE t AS SELECT 1 WITH DATA WITH NO DATA", + "SELECT 1 WITH NO DATA")) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + } + + private static CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(CreateTable table) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + table.accept(new StatementDeParser(out)); + assertEquals(table.toString(), out.toString()); + assertEquals(table.getWithData(), parse(out.toString()).getWithData()); + assertEquals(out.toString(), parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java index aa7574c0aa..26bc31cc71 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.BufferedReader; @@ -24,19 +25,27 @@ import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; +import java.util.stream.Collectors; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.operators.relational.GreaterThan; import net.sf.jsqlparser.parser.CCJSqlParserManager; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColDataType.Signedness; import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; -import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.PartitionBound; import net.sf.jsqlparser.statement.create.table.RowMovementMode; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; import net.sf.jsqlparser.test.TestException; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -51,6 +60,14 @@ public void testCreateTableOrReplace() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(statement); } + @Test + public void testCreateTableCopyGrants() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "CREATE OR REPLACE TABLE testtab COPY GRANTS AS (SELECT a FROM tbl)"); + assertSqlCanBeParsedAndDeparsed( + "CREATE OR REPLACE TABLE testtab (a VARCHAR (255)) COPY GRANTS"); + } + @Test public void testCreateTable2() throws JSQLParserException { String statement = "CREATE TABLE testtab (\"test\" varchar (255))"; @@ -87,8 +104,9 @@ public void testCreateTable() throws JSQLParserException { assertFalse(createTable.isUnlogged()); assertEquals("mycol", createTable.getColumnDefinitions().get(0).getColumnName()); assertEquals("mycol2", createTable.getColumnDefinitions().get(1).getColumnName()); - assertEquals("PRIMARY KEY", createTable.getIndexes().get(0).getType()); - assertEquals("mycol", createTable.getIndexes().get(0).getColumnsNames().get(1)); + assertEquals("PRIMARY KEY", createTable.getTableConstraints().get(0).getType()); + assertEquals("mycol", ((KeyConstraint) createTable.getTableConstraints().get(0)) + .getColumnsNames().get(1)); assertEquals(statement, "" + createTable); } @@ -102,8 +120,9 @@ public void testCreateTableUnlogged() throws JSQLParserException { assertTrue(createTable.isUnlogged()); assertEquals("mycol", createTable.getColumnDefinitions().get(0).getColumnName()); assertEquals("mycol2", createTable.getColumnDefinitions().get(1).getColumnName()); - assertEquals("PRIMARY KEY", createTable.getIndexes().get(0).getType()); - assertEquals("mycol", createTable.getIndexes().get(0).getColumnsNames().get(1)); + assertEquals("PRIMARY KEY", createTable.getTableConstraints().get(0).getType()); + assertEquals("mycol", ((KeyConstraint) createTable.getTableConstraints().get(0)) + .getColumnsNames().get(1)); assertEquals(statement, "" + createTable); } @@ -203,9 +222,9 @@ public void testCreateTableUniqueConstraintAfterPrimaryKey() throws JSQLParserEx CreateTable createTable = (CreateTable) CCJSqlParserUtil.parseStatements(sqlStr).getStatements().get(0); - assertEquals("PRIMARY KEY", createTable.getIndexes().get(0).getType()); - assertEquals("UNIQUE", createTable.getIndexes().get(1).getType()); - assertEquals("FOREIGN KEY", createTable.getIndexes().get(2).getType()); + assertEquals("PRIMARY KEY", createTable.getTableConstraints().get(0).getType()); + assertEquals("UNIQUE", createTable.getTableConstraints().get(1).getType()); + assertEquals("FOREIGN KEY", createTable.getTableConstraints().get(2).getType()); } @Test @@ -467,7 +486,7 @@ public void testExcludeWhereConstraint() throws JSQLParserException { assertDeparse( new CreateTable() .withTable(new Table("foo")) - .addIndexes( + .addTableConstraints( new ExcludeConstraint() .withExpression( new GreaterThan() @@ -627,11 +646,15 @@ public void testRUBiSCreateList() throws Exception { .getColumnDefinitions()) { String colName = columnDefinition.getColumnName(); boolean unique = false; - if (createTable.getIndexes() != null) { - for (Index index : createTable.getIndexes()) { - if (index.getType().equals("PRIMARY KEY") - && index.getColumnsNames().size() == 1 - && index.getColumnsNames().get(0).equals(colName)) { + if (createTable.getTableConstraints() != null) { + for (NamedConstraint constraint : createTable + .getTableConstraints()) { + if (constraint instanceof KeyConstraint + && constraint.getKind() == ConstraintKind.PRIMARY_KEY + && ((KeyConstraint) constraint).getColumnsNames() + .size() == 1 + && ((KeyConstraint) constraint).getColumnsNames().get(0) + .equals(colName)) { unique = true; } } @@ -1175,8 +1198,9 @@ void testCreateTableIndexVisibility() throws JSQLParserException { ") ENGINE = InnoDB"; CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed(sqlStr, true); - assertEquals(Arrays.asList("INVISIBLE"), createTable.getIndexes().get(1).getIndexSpec()); - assertEquals(Arrays.asList("VISIBLE"), createTable.getIndexes().get(2).getIndexSpec()); + assertEquals(Arrays.asList("INVISIBLE"), createTable.getIndexes().get(0).getIndexSpec()); + assertEquals(Arrays.asList("VISIBLE"), + ((KeyConstraint) createTable.getTableConstraints().get(1)).getIndexSpec()); } @Test @@ -1191,6 +1215,36 @@ void testCreateTableColumnVisibility() throws JSQLParserException { createTable.getColumnDefinitions().get(1).getColumnSpecs()); } + @Test + void testOracleInlineConstraintState() throws JSQLParserException { + String sqlStr = + "CREATE TABLE DIAM.IAM_DENEME1 (" + + "ID VARCHAR2(36 BYTE) NOT NULL ENABLE," + + "REQUEST_ID VARCHAR2(50 BYTE)," + + "CREATE_DATE TIMESTAMP(9)," + + "USER_ID VARCHAR2(50 BYTE)," + + "CODE VARCHAR2(100 BYTE) NOT NULL ENABLE," + + "IS_OUTSOURCE_ENABLED CHAR(1 BYTE) DEFAULT 1 NOT NULL ENABLE," + + "IS_PASSWORD_NUMERIC CHAR(1 BYTE) DEFAULT 0 NOT NULL ENABLE" + + ")"; + + CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(sqlStr); + + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE test (" + + "a NUMBER NOT NULL ENABLE," + + "b NUMBER NOT NULL DISABLE," + + "c NUMBER NOT NULL ENABLE NOVALIDATE," + + "d NUMBER CONSTRAINT d_nn NOT NULL DEFERRABLE VALIDATE" + + ")", + true); + + assertEquals(Arrays.asList("NOT", "NULL", "ENABLE"), + createTable.getColumnDefinitions().get(0).getColumnSpecs()); + assertEquals(Arrays.asList("DEFAULT", "1", "NOT", "NULL", "ENABLE"), + createTable.getColumnDefinitions().get(5).getColumnSpecs()); + } + @Test void testCreateTableIndexVisibilityWithOtherIndexOptions() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed( @@ -1217,4 +1271,121 @@ void testUniqueIndexIssue1893() throws JSQLParserException { // A plain INDEX must still parse unchanged. assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a int, INDEX idx (a))", true); } + + @Test + void testMySqlColumnTypeModifiers() throws JSQLParserException { + String sql = "CREATE TABLE t (a INT UNSIGNED, b INT (11) UNSIGNED ZEROFILL, " + + "c DECIMAL (10, 2) SIGNED NOT NULL)"; + CreateTable createTable = + (CreateTable) assertSqlCanBeParsedAndDeparsed(sql, true); + + ColDataType first = createTable.getColumnDefinitions().get(0).getColDataType(); + assertEquals("INT", first.getDataType()); + assertEquals(Signedness.UNSIGNED, first.getSignedness()); + assertFalse(first.isZerofill()); + + ColDataType second = createTable.getColumnDefinitions().get(1).getColDataType(); + assertEquals(Signedness.UNSIGNED, second.getSignedness()); + assertTrue(second.isZerofill()); + + ColDataType third = createTable.getColumnDefinitions().get(2).getColDataType(); + assertEquals(Signedness.SIGNED, third.getSignedness()); + assertEquals(List.of("NOT", "NULL"), + createTable.getColumnDefinitions().get(2).getColumnSpecs()); + } + + @Test + void testMySqlConstraintSymbolAndIndexNameIssue1570() throws JSQLParserException { + String uniqueSql = "CREATE TABLE table1 (col1 INT, col2 INT UNIQUE, " + + "CONSTRAINT my_constraint UNIQUE KEY index_name (col1))"; + CreateTable uniqueTable = (CreateTable) assertSqlCanBeParsedAndDeparsed(uniqueSql, true); + KeyConstraint unique = (KeyConstraint) uniqueTable.getTableConstraints().get(0); + + assertEquals("my_constraint", unique.getName()); + assertEquals("index_name", unique.getIndexName()); + assertEquals("UNIQUE KEY", unique.getType()); + + String foreignKeySql = "CREATE TABLE table2 (id INT, parent_id INT, " + + "CONSTRAINT fk_parent FOREIGN KEY fk_parent_idx (parent_id) " + + "REFERENCES parent (id))"; + CreateTable foreignKeyTable = + (CreateTable) assertSqlCanBeParsedAndDeparsed(foreignKeySql, true); + ForeignKeyConstraint foreignKey = + (ForeignKeyConstraint) foreignKeyTable.getTableConstraints().get(0); + + assertEquals("fk_parent", foreignKey.getName()); + assertEquals("fk_parent_idx", foreignKey.getIndexName()); + } + + @Test + void testMySqlUnnamedConstraintAndUniqueIndexOptionsIssues1570And538() + throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE table1 (col1 INT, CONSTRAINT UNIQUE KEY (col1))", true); + + String sql = "CREATE TABLE g_platform_payway (id INT, platform_code VARCHAR (45), " + + "pay_way VARCHAR (32), UNIQUE uniq_platform_payway USING BTREE " + + "(platform_code, pay_way) COMMENT 'should be unique')"; + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed(sql, true); + KeyConstraint unique = (KeyConstraint) createTable.getTableConstraints().get(0); + + assertNull(unique.getName()); + assertEquals("uniq_platform_payway", unique.getIndexName()); + assertEquals("BTREE", unique.getUsing()); + assertEquals(List.of("COMMENT", "'should be unique'"), unique.getIndexSpec()); + } + + @Test + void testPostgreSqlDeclarativePartitionParentsIssue2522() throws JSQLParserException { + String sql = "CREATE TABLE pg_range_parent (tenant_id int, " + + "happened_at timestamptz) PARTITION BY RANGE (tenant_id, happened_at)"; + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed( + sql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + TablePartitioning partitioning = createTable.getPartitioning(); + + assertEquals(TablePartitioning.Type.RANGE, partitioning.getType()); + assertEquals(List.of("tenant_id", "happened_at"), partitioning.getExpressionList() + .stream().map(Object::toString).collect(Collectors.toList())); + + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE pg_list_parent (region text) PARTITION BY LIST (region)", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE pg_hash_parent (tenant_id int) PARTITION BY HASH (tenant_id)", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + @Test + void testPostgreSqlDeclarativePartitionChildrenIssue2522() throws JSQLParserException { + String rangeSql = "CREATE TABLE pg_range_p1 PARTITION OF pg_range_parent " + + "FOR VALUES FROM (1, MINVALUE) TO (2, MAXVALUE)"; + CreateTable range = (CreateTable) assertSqlCanBeParsedAndDeparsed( + rangeSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals("pg_range_parent", range.getPartitionOf().getName()); + assertEquals(PartitionBound.Type.RANGE, range.getPartitionBound().getType()); + assertEquals(List.of("1", "MINVALUE"), range.getPartitionBound().getFromExpressions() + .stream().map(Object::toString).collect(Collectors.toList())); + assertEquals(List.of("2", "MAXVALUE"), range.getPartitionBound().getToExpressions() + .stream().map(Object::toString).collect(Collectors.toList())); + + String listSql = "CREATE TABLE pg_list_eu_us PARTITION OF pg_list_parent " + + "FOR VALUES IN ('eu', 'us')"; + CreateTable list = (CreateTable) assertSqlCanBeParsedAndDeparsed( + listSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(PartitionBound.Type.LIST, list.getPartitionBound().getType()); + + String hashSql = "CREATE TABLE pg_hash_p0 PARTITION OF pg_hash_parent " + + "FOR VALUES WITH (MODULUS 4, REMAINDER 0)"; + CreateTable hash = (CreateTable) assertSqlCanBeParsedAndDeparsed( + hashSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(PartitionBound.Type.HASH, hash.getPartitionBound().getType()); + assertEquals("4", hash.getPartitionBound().getModulus().toString()); + assertEquals("0", hash.getPartitionBound().getRemainder().toString()); + + String defaultSql = + "CREATE TABLE pg_list_default PARTITION OF pg_list_parent DEFAULT"; + CreateTable defaultPartition = (CreateTable) assertSqlCanBeParsedAndDeparsed( + defaultSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(PartitionBound.Type.DEFAULT, defaultPartition.getPartitionBound().getType()); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/DdlParameterListTest.java b/src/test/java/net/sf/jsqlparser/statement/create/DdlParameterListTest.java new file mode 100644 index 0000000000..5f1dd5f9eb --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/DdlParameterListTest.java @@ -0,0 +1,57 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserConstants; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.ParseException; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DdlParameterListTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE VECTOR INDEX items_hnsw ON items (embedding) ORGANIZATION INMEMORY NEIGHBOR GRAPH DISTANCE COSINE WITH TARGET ACCURACY 95 PARAMETERS (TYPE HNSW,NEIGHBORS 32,EFCONSTRUCTION 500)", + "CREATE VECTOR INDEX items_ivf ON items (embedding) ORGANIZATION NEIGHBOR PARTITIONS DISTANCE COSINE WITH TARGET ACCURACY 95 PARAMETERS (TYPE IVF,NEIGHBOR PARTITIONS 10)", + "CREATE TABLE samples (id INT) STORAGE (INITIAL 65536,NEXT 131072)"}) + void ddlParametersKeepSeparateWordsAndNumbers(String sql) throws JSQLParserException { + for (Dialect dialect : new Dialect[] {null, Dialect.ORACLE}) { + Statement statement = CCJSqlParserUtil.parse(sql, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + assertEquals(sql, statement.toString()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(sql, output.toString()); + assertEquals(sql, CCJSqlParserUtil.parse(output.toString()).toString()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"()", "(1,2,3)", "(name=value,count=32)", + "(TYPE HNSW,NEIGHBORS 32,EFCONSTRUCTION 500)", + "(enabled TRUE,ratio 0.5,disabled FALSE)", + "('keep two spaces','comma,equals=','it''s fine')", + "(\"Quoted Name\" 10,\"dotted.name\" 20)"}) + void preservesLiteralsQuotedNamesAndExplicitDelimiters(String list) throws ParseException { + CCJSqlParser parser = CCJSqlParserUtil.newParser(list); + assertEquals(list, parser.AList()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/DialectIndexDefinitionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/DialectIndexDefinitionTest.java new file mode 100644 index 0000000000..c5860a05e3 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/DialectIndexDefinitionTest.java @@ -0,0 +1,216 @@ +/*- + * #%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.statement.create; + +import net.sf.jsqlparser.statement.create.table.IndexOptions; +import net.sf.jsqlparser.statement.create.table.KeyElement; +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.Index; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DialectIndexDefinitionTest { + private static CreateIndex parse(String sql, Dialect dialect) throws JSQLParserException { + return (CreateIndex) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + @ParameterizedTest + @ValueSource(strings = {"", "UNIQUE ", "CLUSTERED ", "NONCLUSTERED ", + "UNIQUE CLUSTERED ", "UNIQUE NONCLUSTERED "}) + void parsesSqlServerIndexModifiers(String modifiers) throws Exception { + String sql = "CREATE " + modifiers + + "INDEX [index name] ON [dbo].[store] ([manager_staff_id] DESC, [id] ASC)"; + CreateIndex statement = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.SQLSERVER)); + Index index = statement.getIndex(); + assertEquals(modifiers.contains("UNIQUE") ? "UNIQUE" : null, index.getType()); + assertEquals(modifiers.contains("NONCLUSTERED") ? IndexOptions.Clustering.NONCLUSTERED + : modifiers.contains("CLUSTERED") ? IndexOptions.Clustering.CLUSTERED : null, + index.getClustering()); + assertFalse(statement.isNullFiltered()); + assertEquals(index.getClustering(), parse(statement.toString(), Dialect.SQLSERVER) + .getIndex().getClustering()); + } + + @ParameterizedTest + @ValueSource(strings = {"", "UNIQUE ", "NULL_FILTERED ", "UNIQUE NULL_FILTERED "}) + void parsesSpannerIndexModifiersIssue652(String modifiers) throws Exception { + String sql = "CREATE " + modifiers + + "INDEX IF NOT EXISTS `index name` ON `Users` (`name` DESC, `id` ASC)"; + CreateIndex statement = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.SPANNER)); + assertEquals(modifiers.contains("UNIQUE") ? "UNIQUE" : null, + statement.getIndex().getType()); + assertEquals(modifiers.contains("NULL_FILTERED"), statement.isNullFiltered()); + assertNull(statement.getIndex().getClustering()); + assertEquals(statement.isNullFiltered(), + parse(statement.toString(), Dialect.SPANNER).isNullFiltered()); + } + + @Test + void requiresDialectForCombinedModifiersAndPreservesLegacySingleTypes() throws Exception { + for (String type : List.of("CLUSTERED", "NONCLUSTERED", "NULL_FILTERED", "mytype")) { + String sql = "CREATE " + type + " INDEX ix ON t (id)"; + CreateIndex legacy = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + assertEquals(type, legacy.getIndex().getType()); + assertNull(legacy.getIndex().getClustering()); + assertFalse(legacy.isNullFiltered()); + } + for (Dialect target : List.of(Dialect.SQLSERVER, Dialect.SPANNER)) { + String sql = "CREATE UNIQUE " + + (target == Dialect.SQLSERVER ? "NONCLUSTERED" : "NULL_FILTERED") + + " INDEX ix ON t (id)"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != target) { + assertThrows(JSQLParserException.class, () -> parse(sql, dialect), + dialect.name()); + } + } + } + } + + @Test + void rejectsDuplicateOrReversedModifiers() { + for (String modifiers : List.of("UNIQUE CLUSTERED NONCLUSTERED", "NONCLUSTERED UNIQUE", + "UNIQUE UNIQUE", "UNIQUE NULL_FILTERED")) { + assertThrows(JSQLParserException.class, + () -> parse("CREATE " + modifiers + " INDEX ix ON t (id)", Dialect.SQLSERVER)); + } + for (String modifiers : List.of("NULL_FILTERED UNIQUE", + "UNIQUE NULL_FILTERED NULL_FILTERED", + "UNIQUE UNIQUE", "UNIQUE NONCLUSTERED")) { + assertThrows(JSQLParserException.class, + () -> parse("CREATE " + modifiers + " INDEX ix ON t (id)", Dialect.SPANNER)); + } + } + + @Test + void rendersModifiedClusteringAndNullFiltering() throws Exception { + CreateIndex sqlServer = parse("CREATE UNIQUE NONCLUSTERED INDEX ix ON t (id)", + Dialect.SQLSERVER); + sqlServer.getIndex().setClustering(IndexOptions.Clustering.CLUSTERED); + assertDeparse(sqlServer, "CREATE UNIQUE CLUSTERED INDEX ix ON t (id)"); + sqlServer.getIndex().setClustering(null); + assertDeparse(sqlServer, "CREATE UNIQUE INDEX ix ON t (id)"); + + CreateIndex spanner = parse("CREATE UNIQUE NULL_FILTERED INDEX ix ON t (id)", + Dialect.SPANNER); + spanner.setNullFiltered(false); + assertDeparse(spanner, "CREATE UNIQUE INDEX ix ON t (id)"); + assertDeparse(spanner.withNullFiltered(true), + "CREATE UNIQUE NULL_FILTERED INDEX ix ON t (id)"); + } + + @Test + void parsesSakilaIndexAndFollowingStatementsIssue1563() throws Exception { + String sql = + "CREATE UNIQUE NONCLUSTERED INDEX idx_fk_address_id ON store(manager_staff_id);"; + Statements statements = CCJSqlParserUtil.parseStatements(sql + "\nGO\nSELECT 1;", + parser -> parser.withDialect(Dialect.SQLSERVER)); + assertEquals(2, statements.size()); + assertEquals(IndexOptions.Clustering.NONCLUSTERED, + ((CreateIndex) statements.get(0)).getIndex().getClustering()); + assertEquals("SELECT 1", statements.get(1).toString()); + } + + @Test + void parsesQualifiedPostgreSqlAttributesIssue1994() throws Exception { + String sql = "CREATE INDEX \"index_keyword\" ON \"inter\".\"inter_ti_rec\" USING btree " + + "(\"keyword\" COLLATE \"pg_catalog\".\"default\" \"pg_catalog\".\"text_ops\" ASC NULLS LAST)"; + CreateIndex statement = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + KeyElement key = statement.getIndex().getColumns().get(0); + assertEquals("\"pg_catalog\".\"default\"", key.getCollation()); + assertEquals("\"pg_catalog\".\"text_ops\"", key.getOperatorClass()); + assertEquals(KeyElement.SortOrder.ASC, key.getSortOrder()); + assertEquals(KeyElement.NullOrdering.LAST, key.getNullOrdering()); + assertNull(key.getParams()); + + key.setCollation("public.\"C\""); + key.setOperatorClass("public.text_pattern_ops"); + key.setSortOrder(KeyElement.SortOrder.DESC); + key.setNullOrdering(KeyElement.NullOrdering.FIRST); + assertDeparse(statement, + "CREATE INDEX \"index_keyword\" ON \"inter\".\"inter_ti_rec\" USING btree " + + "(\"keyword\" COLLATE public.\"C\" public.text_pattern_ops DESC NULLS FIRST)"); + assertEquals(key.getOperatorClass(), parse(statement.toString(), Dialect.POSTGRESQL) + .getIndex().getColumns().get(0).getOperatorClass()); + key.setCollation(null); + key.setOperatorClass(null); + key.setSortOrder(null); + key.setNullOrdering(null); + assertDeparse(statement, + "CREATE INDEX \"index_keyword\" ON \"inter\".\"inter_ti_rec\" USING btree (\"keyword\")"); + } + + @Test + void preservesQuotedNamePartsAndBareFunctionKeys() throws Exception { + String sql = "CREATE INDEX ix ON t (lower(name) COLLATE \"schema.with.dot\".\"C\" " + + "\"schema\".\"op\"\"class\" DESC NULLS FIRST)"; + CreateIndex statement = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + KeyElement key = statement.getIndex().getColumns().get(0); + assertTrue(key.isExpression()); + assertFalse(key.isExpressionParenthesized()); + assertEquals("\"schema.with.dot\".\"C\"", key.getCollation()); + assertEquals("\"schema\".\"op\"\"class\"", key.getOperatorClass()); + assertNull(key.getParams()); + } + + @Test + void keepsPostgreSqlFunctionAndFollowingIndexSeparateIssue1994() throws Exception { + String sql = "DROP FUNCTION IF EXISTS fin.restore_fund_data(in_fund_id int8); " + + "CREATE OR REPLACE FUNCTION fin.restore_fund_data(in_fund_id int8) " + + "RETURNS pg_catalog.varchar AS $BODY$ BEGIN RETURN 'success'; END $BODY$ " + + "LANGUAGE plpgsql VOLATILE COST 100; " + + "CREATE INDEX ix ON inter.inter_ti_rec " + + "(keyword COLLATE pg_catalog.\"default\" pg_catalog.text_ops ASC NULLS LAST); " + + "SELECT 1;"; + Statements statements = CCJSqlParserUtil.parseStatements(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(4, statements.size()); + assertEquals("pg_catalog.text_ops", ((CreateIndex) statements.get(2)) + .getIndex().getColumns().get(0).getOperatorClass()); + assertEquals("SELECT 1", statements.get(3).toString()); + } + + @Test + void gatesQualifiedAttributesAndRejectsRepeatedPostgreSqlAttributes() { + for (String key : List.of("name COLLATE public.\"C\"", "name public.text_ops")) { + String sql = "CREATE INDEX ix ON t (" + key + ")"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.POSTGRESQL) { + assertThrows(JSQLParserException.class, () -> parse(sql, dialect)); + } + } + } + for (String key : List.of("name ASC DESC", "name NULLS FIRST NULLS LAST", + "name COLLATE public.\"C\" COLLATE public.\"C\"")) { + assertThrows(JSQLParserException.class, + () -> parse("CREATE INDEX ix ON t (" + key + ")", Dialect.POSTGRESQL)); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ForeignKeyReferenceMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ForeignKeyReferenceMutationTest.java new file mode 100644 index 0000000000..d86855e797 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ForeignKeyReferenceMutationTest.java @@ -0,0 +1,197 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.ReferentialAction; +import net.sf.jsqlparser.statement.ReferentialAction.Action; +import net.sf.jsqlparser.statement.ReferentialAction.Type; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.ForeignKeyReference; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ForeignKeyReferenceMutationTest { + @Test + void keepsLegacyAndStructuredAccessOnTheSameValues() { + ForeignKeyConstraint index = new ForeignKeyConstraint(); + Table table = new Table("parent"); + List columns = new ArrayList<>(List.of("id")); + index.setTable(table); + index.setReferencedColumnNames(columns); + index.setReferentialAction(Type.DELETE, Action.CASCADE); + + ForeignKeyReference reference = index.getReference(); + assertSame(table, reference.getTable()); + assertSame(columns, reference.getReferencedColumnNames()); + assertEquals(Action.CASCADE, reference.getReferentialAction(Type.DELETE).getAction()); + + Table replacement = new Table("other_parent"); + reference.setTable(replacement); + reference.setReferencedColumnNames(new ArrayList<>(List.of("other_id"))); + reference.setReferentialAction(Type.DELETE, Action.RESTRICT); + assertSame(replacement, index.getTable()); + assertSame(reference.getReferencedColumnNames(), index.getReferencedColumnNames()); + assertEquals(Action.RESTRICT, index.getReferentialAction(Type.DELETE).getAction()); + + index.addReferencedColumnNames("tenant_id"); + index.getReferencedColumnNames().remove("other_id"); + assertEquals(List.of("tenant_id"), reference.getReferencedColumnNames()); + index.removeReferentialAction(Type.DELETE); + assertNull(reference.getReferentialAction(Type.DELETE)); + reference.getReferentialActions().add(new ReferentialAction(Type.UPDATE, Action.CASCADE)); + assertSame(reference.getReferentialAction(Type.UPDATE), + index.getReferentialAction(Type.UPDATE)); + } + + @Test + void preservesNullableLegacyFieldsAndAnEmptyReference() { + ForeignKeyConstraint index = new ForeignKeyConstraint(); + index.setReference(null); + assertNull(index.getTable()); + assertNull(index.getReferencedColumnNames()); + assertNull(index.getReferentialAction(Type.DELETE)); + assertNull(index.getMatchType()); + index.setTable(new Table("parent")); + index.setReferencedColumnNames(new ArrayList<>(List.of("id"))); + index.setTable(null); + index.setReferencedColumnNames(null); + assertNull(index.getReference().getTable()); + assertNull(index.getReference().getReferencedColumnNames()); + } + + @Test + void clearingReferenceRetainsLegacyValuesAndDiscardsReferenceOnlyOptions() { + ForeignKeyReference reference = new ForeignKeyReference() + .withTable(new Table("parent")) + .withReferencedColumnNames(new ArrayList<>(List.of("id"))) + .withMatchType(ForeignKeyReference.MatchType.FULL) + .withUsingPeriod(true) + .withReferentialAction(Type.DELETE, Action.CASCADE); + reference.setConstraintName("column_fk"); + reference.setConstraintAttributes(new ConstraintAttributes()); + ForeignKeyConstraint index = new ForeignKeyConstraint().withReference(reference); + + index.setReference(null); + + assertSame(reference.getTable(), index.getTable()); + assertSame(reference.getReferencedColumnNames(), index.getReferencedColumnNames()); + assertEquals(Action.CASCADE, index.getReferentialAction(Type.DELETE).getAction()); + assertNull(index.getMatchType()); + ForeignKeyReference detached = index.getReference(); + assertNotSame(reference, detached); + assertFalse(detached.isUsingPeriod()); + assertNull(detached.getConstraintName()); + assertNull(detached.getConstraintAttributes()); + } + + @Test + void clearingReferenceRetainsTheLatestStructuredValues() { + ForeignKeyReference reference = new ForeignKeyReference() + .withTable(new Table("parent")) + .withReferencedColumnNames(new ArrayList<>(List.of("id"))); + ForeignKeyConstraint index = new ForeignKeyConstraint().withReference(reference); + Table replacement = new Table("other_parent"); + List columns = new ArrayList<>(List.of("other_id")); + reference.setTable(replacement); + reference.setReferencedColumnNames(columns); + reference.setReferentialAction(Type.DELETE, Action.SET_NULL); + reference.getReferentialAction(Type.DELETE).setColumnNames(List.of("child_id")); + + index.setReference(null); + + assertSame(replacement, index.getTable()); + assertSame(columns, index.getReferencedColumnNames()); + assertEquals(List.of("child_id"), index.getReferentialAction(Type.DELETE).getColumnNames()); + assertEquals(List.of("child_id"), + index.getReference().getReferentialAction(Type.DELETE).getColumnNames()); + } + + @Test + void accessingReferenceDoesNotReplaceActionsOrDiscardTheirColumns() { + ForeignKeyConstraint index = new ForeignKeyConstraint(); + index.setReferentialAction(Type.DELETE, Action.SET_NULL); + ReferentialAction action = index.getReferentialAction(Type.DELETE); + action.setColumnNames(List.of("child_id")); + + assertSame(action, index.getReference().getReferentialAction(Type.DELETE)); + assertEquals(List.of("child_id"), index.getReferentialAction(Type.DELETE).getColumnNames()); + action.setAction(Action.SET_DEFAULT); + assertEquals(Action.SET_DEFAULT, + index.getReference().getReferentialAction(Type.DELETE).getAction()); + } + + @Test + @SuppressWarnings("deprecation") + void deprecatedActionSettersUpdateStructuredReference() { + ForeignKeyConstraint index = new ForeignKeyConstraint(); + ForeignKeyReference reference = index.getReference(); + index.setOnDeleteReferenceOption("CASCADE"); + index.setOnUpdateReferenceOption("RESTRICT"); + assertEquals(Action.CASCADE, reference.getReferentialAction(Type.DELETE).getAction()); + assertEquals("RESTRICT", index.getOnUpdateReferenceOption()); + reference.setReferentialAction(Type.UPDATE, Action.CASCADE); + assertEquals("CASCADE", index.getOnUpdateReferenceOption()); + index.setOnDeleteReferenceOption(null); + index.setOnUpdateReferenceOption(null); + assertNull(reference.getReferentialAction(Type.DELETE)); + assertNull(reference.getReferentialAction(Type.UPDATE)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void mutationsSurviveBothRenderersAndReparsing(boolean create) throws JSQLParserException { + String prefix = create ? "CREATE TABLE child (parent_id INT, " : "ALTER TABLE child ADD "; + Statement statement = CCJSqlParserUtil.parse(prefix + + "CONSTRAINT fk FOREIGN KEY (parent_id) REFERENCES parent (id) ON DELETE CASCADE" + + (create ? ")" : "")); + ForeignKeyConstraint index = foreignKey(statement); + ForeignKeyReference reference = index.getReference(); + reference.setTable(new Table("other_parent")); + reference.getReferencedColumnNames().set(0, "other_id"); + index.setReferentialAction(Type.DELETE, Action.SET_NULL); + index.getReferentialAction(Type.DELETE).setColumnNames(List.of("parent_id")); + index.setMatchType(ForeignKeyReference.MatchType.SIMPLE); + + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(statement.toString(), deparsed.toString()); + Statement reparsed = CCJSqlParserUtil.parse(deparsed.toString()); + assertEquals(deparsed.toString(), reparsed.toString()); + ForeignKeyConstraint result = foreignKey(reparsed); + assertEquals("other_parent", result.getTable().getName()); + assertEquals(List.of("other_id"), result.getReferencedColumnNames()); + assertEquals(List.of("parent_id"), + result.getReferentialAction(Type.DELETE).getColumnNames()); + assertEquals(ForeignKeyReference.MatchType.SIMPLE, result.getMatchType()); + } + + private static ForeignKeyConstraint foreignKey(Statement statement) { + return (ForeignKeyConstraint) (statement instanceof CreateTable + ? ((CreateTable) statement).getTableConstraints().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getConstraint()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/IdentitySequenceNameTest.java b/src/test/java/net/sf/jsqlparser/statement/create/IdentitySequenceNameTest.java new file mode 100644 index 0000000000..be75c34061 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/IdentitySequenceNameTest.java @@ -0,0 +1,76 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Sequence; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.IdentityDefinition; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class IdentitySequenceNameTest { + @ParameterizedTest + @ValueSource(strings = {"ids", "public.ids", "\"schema.name\".\"seq.name\""}) + void sharesIdentityOptionsAcrossCreateAndAlter(String name) throws JSQLParserException { + String definition = "GENERATED BY DEFAULT AS IDENTITY (START 4 SEQUENCE NAME " + name + + " CACHE 2 NO CYCLE)"; + CreateTable create = (CreateTable) parse("CREATE TABLE t (id INT " + definition + ")"); + IdentityDefinition identity = create.getColumnDefinitions().get(0).getColumnOptions().get(0) + .getIdentityDefinition(); + Alter alter = (Alter) parse("ALTER TABLE t ALTER COLUMN id ADD " + definition); + IdentityDefinition altered = alter.getAlterExpressions().get(0).getColDataTypeList().get(0) + .getIdentityAlterations().get(0).getIdentityDefinition(); + for (IdentityDefinition current : new IdentityDefinition[] {identity, altered}) { + assertEquals(4, current.getParameters().size()); + Sequence.Parameter named = current.getParameters().get(1); + assertEquals(Sequence.ParameterType.SEQUENCE_NAME, named.getOption()); + assertEquals(name, named.getSequenceName().getFullyQualifiedName()); + assertNull(named.getValue()); + named.getSequenceName().setName("renamed"); + } + assertTrue(create.toString().contains("renamed")); + assertTrue(alter.toString().contains("renamed")); + roundTrip(create); + roundTrip(alter); + } + + @Test + void preservesNameOnlyAndDoesNotExtendStandaloneSequenceOptions() throws JSQLParserException { + String sql = "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME ids))"; + assertEquals(sql, parse(sql).toString()); + roundTrip(parse(sql)); + for (String invalid : new String[] { + "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME))", + "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY ())", + "CREATE SEQUENCE ids SEQUENCE NAME other_ids", + "ALTER TABLE t ALTER COLUMN id SET SEQUENCE NAME other_ids"}) { + assertThrows(JSQLParserException.class, () -> parse(invalid), invalid); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/IndexKindStateTest.java b/src/test/java/net/sf/jsqlparser/statement/create/IndexKindStateTest.java new file mode 100644 index 0000000000..102d52e54d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/IndexKindStateTest.java @@ -0,0 +1,105 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.DefaultConstraint; +import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.Index.Kind; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class IndexKindStateTest { + @ParameterizedTest + @CsvSource({"unique key, UNIQUE", "UNIQUE INDEX, UNIQUE", "KEY, INDEX", "INDEX, INDEX", + "FULLTEXT KEY, FULLTEXT", "SPATIAL INDEX, SPATIAL", "BITMAP INDEX, INDEX"}) + void replacingIndexTypeRefreshesEveryPreviousClassification(String type, Kind expected) { + for (Kind previous : Kind.values()) { + Index index = new Index().withType("UNIQUE").withKind(previous); + index.setType(type); + assertEquals(type, index.getType()); + assertEquals(expected, index.getKind(), previous + " -> " + type); + } + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"CUSTOM", "UNIQUE_CUSTOM", "PRIMARY_CUSTOM", "MONKEY", "INDEXED", + "PRIMARY KEY", "FOREIGN KEY", "CHECK", "EXCLUDE", "DEFAULT", "NOT NULL"}) + void constraintAndUnknownTypesAreNotPhysicalIndexKinds(String type) { + Index index = new Index().withType("UNIQUE").withType(type); + assertEquals(type, index.getType()); + assertEquals(Kind.OTHER, index.getKind()); + } + + @Test + void classificationDoesNotRewriteSpelling() { + Index index = new Index().withType(" unique\tkey "); + assertEquals(Kind.UNIQUE, index.getKind()); + assertEquals(" unique\tkey ", index.getType()); + KeyConstraint constraint = new KeyConstraint().withType(" unique\tkey "); + assertEquals(ConstraintKind.UNIQUE, constraint.getKind()); + constraint.setKind(ConstraintKind.PRIMARY_KEY); + assertEquals(" unique\tkey ", constraint.getType()); + constraint.setType("PRIMARY KEY"); + assertEquals(ConstraintKind.PRIMARY_KEY, constraint.getKind()); + } + + @Test + void metadataAndFixedConstraintKindsRemainSeparate() { + Index index = new Index().withKind(Kind.INDEX); + assertNull(index.getType()); + assertEquals(Kind.INDEX, index.getKind()); + assertEquals(ConstraintKind.CHECK, new CheckConstraint().getKind()); + assertEquals(ConstraintKind.EXCLUDE, new ExcludeConstraint().getKind()); + assertEquals(ConstraintKind.DEFAULT, new DefaultConstraint().getKind()); + } + + @ParameterizedTest + @CsvSource({"UNIQUE, PRIMARY KEY, PRIMARY_KEY", "PRIMARY KEY, UNIQUE, UNIQUE"}) + void rewritingConstraintPreservesKindThroughDeparsing(String before, String after, + ConstraintKind expected) throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE index_state (id INT, CONSTRAINT key_state " + before + " (id))"); + KeyConstraint constraint = (KeyConstraint) table.getTableConstraints().get(0); + constraint.setType(after); + assertEquals(expected, constraint.getKind()); + CreateTable reparsed = (CreateTable) assertSqlCanBeParsedAndDeparsed(table.toString()); + assertEquals(expected, reparsed.getTableConstraints().get(0).getKind()); + assertEquals("key_state", reparsed.getTableConstraints().get(0).getName()); + assertEquals(0, reparsed.getIndexes().size()); + } + + @ParameterizedTest + @ValueSource(strings = {"UNIQUE", "UNIQUE KEY", "UNIQUE INDEX"}) + void tableUniquenessRequiresAConstraintNodeRatherThanChangingAnIndexType(String type) + throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE index_state (id INT, KEY key_state (id))"); + table.getIndexes().clear(); + table.getTableConstraints().add(new KeyConstraint().withType(type) + .withIndexName("key_state").withColumnsNames(java.util.List.of("id"))); + CreateTable reparsed = (CreateTable) assertSqlCanBeParsedAndDeparsed(table.toString()); + assertEquals(0, reparsed.getIndexes().size()); + assertEquals(ConstraintKind.UNIQUE, reparsed.getTableConstraints().get(0).getKind()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/KeyColumnSourceMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/KeyColumnSourceMutationTest.java new file mode 100644 index 0000000000..e2dbd4005f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/KeyColumnSourceMutationTest.java @@ -0,0 +1,88 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class KeyColumnSourceMutationTest { + @ParameterizedTest + @ValueSource(strings = {"INDEX ix (id DESC)", "PRIMARY KEY (id)", "UNIQUE (id DESC)", + "FOREIGN KEY (id) REFERENCES parent (id)", "EXCLUDE USING btree (id WITH =)"}) + void clearedNamesCanBeRepopulatedThroughBothAppendApisAndTheLiveList(String declaration) + throws Exception { + for (boolean nullClear : new boolean[] {true, false}) { + for (boolean collectionAppend : new boolean[] {false, true}) { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT, " + declaration + ")"); + KeyColumnSource key = (KeyColumnSource) table.getTableElements().get(1); + KeyElement element = key.getColumns().get(0); + String expected = table.toString(); + + key.setColumnsNames(nullClear ? null : List.of()); + assertNotNull(key.getColumns()); + assertTrue(key.getColumns().isEmpty()); + assertTrue(key.getColumnsNames().isEmpty()); + append(key, element, collectionAppend); + assertSame(element, key.getColumns().get(0)); + assertSql(table, expected); + + // Clearing and editing through the shared structured API must also work. + key.setColumnsNames(null); + key.getColumns().add(element); + assertSql(table, expected); + } + } + } + + private static void append(KeyColumnSource key, KeyElement element, boolean collection) { + if (key instanceof Index) { + if (collection) { + ((Index) key).addColumns(List.of(element)); + } else { + ((Index) key).addColumns(element); + } + } else if (key instanceof KeyConstraint) { + if (collection) { + ((KeyConstraint) key).addColumns(List.of(element)); + } else { + ((KeyConstraint) key).addColumns(element); + } + } else if (key instanceof ForeignKeyConstraint) { + if (collection) { + ((ForeignKeyConstraint) key).addColumns(List.of(element)); + } else { + ((ForeignKeyConstraint) key).addColumns(element); + } + } else if (key instanceof ExcludeConstraint) { + if (collection) { + ((ExcludeConstraint) key).addColumns(List.of(element)); + } else { + ((ExcludeConstraint) key).addColumns(element); + } + } else { + fail("Unexpected key owner: " + key.getClass()); + } + } + + private static void assertSql(CreateTable table, String expected) throws Exception { + assertEquals(expected, table.toString()); + StringBuilder output = new StringBuilder(); + table.accept(new StatementDeParser(output), null); + assertEquals(expected, output.toString()); + assertEquals(expected, CCJSqlParserUtil.parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTableConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTableConstraintTest.java new file mode 100644 index 0000000000..fda2b37a19 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTableConstraintTest.java @@ -0,0 +1,39 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import org.junit.jupiter.api.Test; + +/** MySQL {@code CREATE TABLE} constraint syntax verified against MySQL 8.4.11. */ +public class MySQLCreateTableConstraintTest { + + @Test + public void testCheckConstraintEnforcement() throws JSQLParserException { + String sql = "CREATE TABLE mysql_check_enforcement (score INT, " + + "CONSTRAINT chk_positive CHECK (score >= 0) ENFORCED, " + + "CONSTRAINT chk_cap CHECK (score < 100) NOT ENFORCED)"; + + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed(sql); + + CheckConstraint positive = (CheckConstraint) createTable.getTableConstraints().get(0); + assertEquals(Boolean.TRUE, positive.getEnforced()); + assertEquals("chk_positive", positive.getName()); + + CheckConstraint cap = (CheckConstraint) createTable.getTableConstraints().get(1); + assertEquals(Boolean.FALSE, cap.getEnforced()); + assertEquals("chk_cap", cap.getName()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTablePartitionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTablePartitionTest.java new file mode 100644 index 0000000000..ce1a0f84cc --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTablePartitionTest.java @@ -0,0 +1,175 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** MySQL 8.4 {@code CREATE TABLE} partition syntax verified against MySQL 8.4.11. */ +public class MySQLCreateTablePartitionTest { + + private static final List MYSQL84_PARTITION_STATEMENTS = List.of( + "CREATE TABLE mysql_partition_hash (id INT) " + + "PARTITION BY HASH (id) PARTITIONS 4", + "CREATE TABLE mysql_partition_linear_hash (id INT) " + + "PARTITION BY LINEAR HASH (id) PARTITIONS 4", + "CREATE TABLE mysql_partition_key (id INT) " + + "PARTITION BY LINEAR KEY ALGORITHM = 1 (id) PARTITIONS 4", + "CREATE TABLE mysql_partition_range (id INT) PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (10), " + + "PARTITION p1 VALUES LESS THAN (20), " + + "PARTITION pmax VALUES LESS THAN MAXVALUE)", + "CREATE TABLE mysql_partition_range_columns (a INT, b INT) " + + "PARTITION BY RANGE COLUMNS (a, b) " + + "(PARTITION p0 VALUES LESS THAN (5, 12), " + + "PARTITION pmax VALUES LESS THAN (MAXVALUE, MAXVALUE))", + "CREATE TABLE mysql_partition_list (id INT) PARTITION BY LIST (id) " + + "(PARTITION p0 VALUES IN (1, 2), " + + "PARTITION p1 VALUES IN (3, 4))", + "CREATE TABLE mysql_partition_list_columns_multi " + + "(region VARCHAR (16), tier INT) " + + "PARTITION BY LIST COLUMNS (region, tier) " + + "(PARTITION p0 VALUES IN (('eu', 1), ('us', 2)), " + + "PARTITION p1 VALUES IN (('apac', 3)))", + "CREATE TABLE mysql_partition_sub_count (id INT, d DATE) " + + "PARTITION BY RANGE (YEAR(d)) " + + "SUBPARTITION BY HASH (id) SUBPARTITIONS 2 " + + "(PARTITION p0 VALUES LESS THAN (2025), " + + "PARTITION pmax VALUES LESS THAN MAXVALUE)", + "CREATE TABLE mysql_partition_sub_explicit (id INT, d DATE) " + + "PARTITION BY RANGE (YEAR(d)) " + + "SUBPARTITION BY LINEAR KEY ALGORITHM = 2 (id) " + + "(PARTITION p0 VALUES LESS THAN (2025) " + + "(SUBPARTITION p0s0 ENGINE = InnoDB, " + + "SUBPARTITION p0s1 ENGINE = InnoDB), " + + "PARTITION pmax VALUES LESS THAN MAXVALUE " + + "(SUBPARTITION pmaxs0 ENGINE = InnoDB, " + + "SUBPARTITION pmaxs1 ENGINE = InnoDB))", + "CREATE TABLE mysql_partition_options (id INT) ENGINE = InnoDB " + + "PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (10) ENGINE = InnoDB " + + "COMMENT = 'hot' MAX_ROWS = 100 MIN_ROWS = 1, " + + "PARTITION pmax VALUES LESS THAN MAXVALUE " + + "ENGINE InnoDB COMMENT 'cold')", + "CREATE TABLE mysql_partition_key_empty " + + "(id INT NOT NULL PRIMARY KEY, name VARCHAR (20)) " + + "PARTITION BY KEY () PARTITIONS 2", + "CREATE TABLE mysql_partition_hash_named (id INT) " + + "PARTITION BY HASH (id) PARTITIONS 2 " + + "(PARTITION p0 STORAGE ENGINE = InnoDB, " + + "PARTITION p1 ENGINE InnoDB)"); + + private static Stream mysql84PartitionStatements() { + return MYSQL84_PARTITION_STATEMENTS.stream(); + } + + @ParameterizedTest + @MethodSource("mysql84PartitionStatements") + public void testMySQL84CreateTablePartitions(String sql) throws JSQLParserException { + assertInstanceOf(CreateTable.class, assertSqlCanBeParsedAndDeparsed(sql)); + } + + @Test + public void testHashAndKeyPartitioningAst() throws JSQLParserException { + CreateTable hash = parse(MYSQL84_PARTITION_STATEMENTS.get(1)); + TablePartitioning hashPartitioning = hash.getPartitioning(); + assertEquals(TablePartitioning.Type.HASH, hashPartitioning.getType()); + assertTrue(hashPartitioning.isLinear()); + assertEquals("id", hashPartitioning.getExpression().toString()); + assertEquals(4L, hashPartitioning.getPartitions()); + + CreateTable key = parse(MYSQL84_PARTITION_STATEMENTS.get(2)); + TablePartitioning keyPartitioning = key.getPartitioning(); + assertEquals(TablePartitioning.Type.KEY, keyPartitioning.getType()); + assertTrue(keyPartitioning.isLinear()); + assertEquals(1, keyPartitioning.getAlgorithm()); + assertTrue(keyPartitioning.isAlgorithmUseEquals()); + assertEquals("id", keyPartitioning.getColumns().get(0).getColumnName()); + + CreateTable emptyKey = parse(MYSQL84_PARTITION_STATEMENTS.get(10)); + assertTrue(emptyKey.getPartitioning().getColumns().isEmpty()); + } + + @Test + public void testRangeAndListPartitionDefinitionsAst() throws JSQLParserException { + CreateTable range = parse(MYSQL84_PARTITION_STATEMENTS.get(4)); + TablePartitioning rangePartitioning = range.getPartitioning(); + assertEquals(TablePartitioning.Type.RANGE, rangePartitioning.getType()); + assertTrue(rangePartitioning.isColumnsSyntax()); + assertEquals("a", rangePartitioning.getColumns().get(0).getColumnName()); + assertEquals("b", rangePartitioning.getColumns().get(1).getColumnName()); + PartitionDefinition maxPartition = rangePartitioning.getPartitionDefinitions().get(1); + assertEquals(PartitionDefinition.ValueOperator.LESS_THAN, + maxPartition.getValueOperator()); + assertEquals(List.of("MAXVALUE", "MAXVALUE"), maxPartition.getValues()); + assertFalse(maxPartition.isMaxValue()); + + CreateTable list = parse(MYSQL84_PARTITION_STATEMENTS.get(5)); + PartitionDefinition listPartition = list.getPartitioning().getPartitionDefinitions().get(0); + assertEquals(PartitionDefinition.ValueOperator.IN, listPartition.getValueOperator()); + assertEquals(List.of("1", "2"), listPartition.getValues()); + } + + @Test + public void testSubPartitioningAst() throws JSQLParserException { + CreateTable createTable = parse(MYSQL84_PARTITION_STATEMENTS.get(8)); + TablePartitioning subPartitioning = createTable.getPartitioning().getSubPartitioning(); + assertNotNull(subPartitioning); + assertEquals(TablePartitioning.Type.KEY, subPartitioning.getType()); + assertTrue(subPartitioning.isLinear()); + assertEquals(2, subPartitioning.getAlgorithm()); + assertEquals("id", subPartitioning.getColumns().get(0).getColumnName()); + + PartitionDefinition first = createTable.getPartitioning().getPartitionDefinitions().get(0); + assertEquals(2, first.getSubPartitionDefinitions().size()); + assertTrue(first.getSubPartitionDefinitions().get(0).isSubPartition()); + assertEquals("InnoDB", first.getSubPartitionDefinitions().get(0).getStorageEngine()); + } + + @Test + public void testPartitionOptionsAst() throws JSQLParserException { + CreateTable createTable = parse(MYSQL84_PARTITION_STATEMENTS.get(9)); + PartitionDefinition first = createTable.getPartitioning().getPartitionDefinitions().get(0); + assertEquals("InnoDB", first.getStorageEngine()); + assertTrue(first.isStorageEngineUseEquals()); + assertEquals("'hot'", first.getComment()); + assertTrue(first.isCommentUseEquals()); + assertEquals("100", first.getMaxRows()); + assertEquals("1", first.getMinRows()); + + PartitionDefinition max = createTable.getPartitioning().getPartitionDefinitions().get(1); + assertTrue(max.isMaxValue()); + assertFalse(max.isStorageEngineUseEquals()); + assertFalse(max.isCommentUseEquals()); + + PartitionDefinition namedHash = parse(MYSQL84_PARTITION_STATEMENTS.get(11)) + .getPartitioning().getPartitionDefinitions().get(0); + assertTrue(namedHash.isStorageKeyword()); + } + + private static CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySQLIndexOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySQLIndexOptionsTest.java new file mode 100644 index 0000000000..efb30097f5 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySQLIndexOptionsTest.java @@ -0,0 +1,98 @@ +/*- + * #%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.statement.create; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.statement.lock.LockStatement; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.CreateIndexDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MySQLIndexOptionsTest { + @Test + void parsesAlgorithmAndLockOptionsInEitherOrder() throws Exception { + for (String algorithm : List.of("DEFAULT", "INPLACE", "COPY")) { + for (String lock : List.of("DEFAULT", "NONE", "SHARED", "EXCLUSIVE")) { + for (String equals : List.of(" ", " = ")) { + String algorithmOption = "ALGORITHM" + equals + algorithm; + String lockOption = "LOCK" + equals + lock; + for (String options : List.of(algorithmOption + " " + lockOption, + lockOption + " " + algorithmOption)) { + for (String statement : List.of("CREATE INDEX idx ON t (id) ", + "DROP INDEX idx ON t ")) { + TestUtils.assertSqlCanBeParsedAndDeparsed(statement + options, true); + } + } + } + } + } + } + + @Test + void preservesLegacyOptionTokensAndTheirOrder() throws Exception { + CreateIndex create = (CreateIndex) CCJSqlParserUtil.parse( + "CREATE INDEX idx ON t (id) LOCK EXCLUSIVE ALGORITHM = DEFAULT"); + assertEquals(List.of("LOCK", "EXCLUSIVE", "ALGORITHM", "=", "DEFAULT"), + create.getTailParameters()); + Drop drop = (Drop) CCJSqlParserUtil.parse( + "DROP INDEX idx ON t LOCK = DEFAULT ALGORITHM COPY"); + assertEquals(List.of("ON", "t", "LOCK", "=", "DEFAULT", "ALGORITHM", "COPY"), + drop.getParameters()); + assertEquals(create.toString(), CCJSqlParserUtil.parse(create.toString()).toString()); + assertEquals(drop.toString(), CCJSqlParserUtil.parse(drop.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE INDEX idx ON t (id)", "DROP INDEX idx ON t"}) + void keepsFollowingLockStatementSeparate(String index) throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + index + "; LOCK TABLE t IN SHARE MODE"); + assertEquals(2, statements.size()); + assertInstanceOf(LockStatement.class, statements.get(1)); + net.sf.jsqlparser.parser.CCJSqlParser parser = CCJSqlParserUtil.newParser( + index + " LOCK TABLE t IN SHARE MODE"); + parser.SingleStatement(); + assertEquals(net.sf.jsqlparser.parser.CCJSqlParserConstants.K_LOCK, + parser.getNextToken().kind); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE INDEX idx ON t (id) LOCK", "DROP INDEX idx ON t LOCK =", + "CREATE INDEX idx ON t (id) ALGORITHM =", "DROP INDEX idx ON t ALGORITHM"}) + void rejectsMissingOptionValues(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE INDEX idx ON t (id) LOCK EXCLUSIVE ALGORITHM DEFAULT", + "CREATE INDEX idx USING BTREE ON t (id) KEY_BLOCK_SIZE = 8 COMMENT 'test' INVISIBLE", + "CREATE INDEX idx ON t (id) parallel compress nologging", + "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx ON ONLY t USING btree (id DESC) INCLUDE (value) NULLS NOT DISTINCT WITH (fillfactor = 80) TABLESPACE fast WHERE active" + }) + void sharesRenderingForMySqlPostgreSqlAndLegacyTails(String sql) throws Exception { + CreateIndex create = (CreateIndex) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + create.getIndex().setName("renamed"); + StringBuilder deparsed = new StringBuilder("prefix "); + new CreateIndexDeParser(deparsed).deParse(create); + assertEquals("prefix " + create, deparsed.toString()); + assertEquals(create.toString(), CCJSqlParserUtil.parse(create.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlColumnAttributeTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlColumnAttributeTest.java new file mode 100644 index 0000000000..b535ff5a5e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlColumnAttributeTest.java @@ -0,0 +1,277 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.ColumnOption.ColumnFormat; +import net.sf.jsqlparser.statement.create.table.ConstraintDeclaration; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlColumnAttributeTest { + private static final Dialect[] DIALECTS = {null, Dialect.MYSQL}; + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t4 (id INT PRIMARY KEY, g GEOMETRY NOT NULL SRID 4326)", + "CREATE TABLE ty5 (c3 MULTIPOLYGON SRID 0, c4 LINESTRING)", + "CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT, g POINT SRID 4326 NOT NULL)", + "CREATE TABLE t18 (a INT COLUMN_FORMAT FIXED STORAGE DISK, b INT COLUMN_FORMAT DYNAMIC, " + + "c INT COLUMN_FORMAT DEFAULT STORAGE MEMORY)", + "CREATE TABLE t19 (a INT ENGINE_ATTRIBUTE '{\"k\":1}' SECONDARY_ENGINE_ATTRIBUTE '{}', " + + "b JSON ENGINE_ATTRIBUTE = '{}' SECONDARY_ENGINE_ATTRIBUTE = '{}')", + "CREATE TABLE t6 (id INT NOT NULL PRIMARY KEY, code VARCHAR (10) UNIQUE KEY, " + + "qty INT CHECK (qty > 0))", + "CREATE TABLE t1 (code VARCHAR (10) UNIQUE)", "CREATE TABLE t1 (id INT KEY)", + "CREATE TABLE t1 (id INT KEY AUTO_INCREMENT, code INT UNIQUE KEY NOT NULL)", + "ALTER TABLE t1 ADD COLUMN geo POINT NOT NULL SRID 4326", + "ALTER TABLE t1 MODIFY COLUMN c INT COLUMN_FORMAT FIXED ENGINE_ATTRIBUTE = '{}'", + "ALTER TABLE t1 ADD COLUMN code INT UNIQUE KEY", + "ALTER TABLE t1 CHANGE COLUMN a id INT KEY"}) + void roundTripsWithAndWithoutDialect(String sql) throws JSQLParserException { + for (Dialect dialect : DIALECTS) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, dialect(dialect)); + assertTrue(columns(statement).stream().flatMap(MySqlColumnAttributeTest::options) + .noneMatch(option -> option.getKind() == ColumnOption.Kind.OTHER), sql); + } + } + + @Test + void exposesTypedAttributesWithLegacyTokens() throws JSQLParserException { + for (Dialect dialect : DIALECTS) { + CreateTable table = (CreateTable) parse("CREATE TABLE t (" + + "g GEOMETRY NOT NULL SRID 4326, a INT COLUMN_FORMAT FIXED STORAGE DISK, " + + "b INT COLUMN_FORMAT DEFAULT, c JSON ENGINE_ATTRIBUTE '{\"k\":1}' " + + "SECONDARY_ENGINE_ATTRIBUTE = '{}')", dialect); + List columns = table.getColumnDefinitions(); + + ColumnOption srid = columns.get(0).getColumnOptions().get(1); + assertEquals(ColumnOption.Kind.SRID, srid.getKind()); + assertEquals(4326L, srid.getSrid()); + assertEquals(List.of("NOT", "NULL", "SRID", "4326"), columns.get(0).getColumnSpecs()); + + ColumnOption fixed = columns.get(1).getColumnOptions().get(0); + assertEquals(ColumnOption.Kind.COLUMN_FORMAT, fixed.getKind()); + assertEquals(ColumnFormat.FIXED, fixed.getColumnFormat()); + assertEquals(ColumnOption.Kind.STORAGE, + columns.get(1).getColumnOptions().get(1).getKind()); + assertEquals(ColumnFormat.DEFAULT, + columns.get(2).getColumnOptions().get(0).getColumnFormat()); + assertEquals(List.of("COLUMN_FORMAT", "DEFAULT"), columns.get(2).getColumnSpecs()); + + ColumnOption engine = columns.get(3).getColumnOptions().get(0); + ColumnOption secondary = columns.get(3).getColumnOptions().get(1); + assertEquals(ColumnOption.Kind.ENGINE_ATTRIBUTE, engine.getKind()); + assertEquals("{\"k\":1}", engine.getEngineAttribute().getValue()); + assertFalse(engine.isUseEquals()); + assertEquals(ColumnOption.Kind.SECONDARY_ENGINE_ATTRIBUTE, secondary.getKind()); + assertTrue(secondary.isUseEquals()); + assertEquals(List.of("ENGINE_ATTRIBUTE", "'{\"k\":1}'", "SECONDARY_ENGINE_ATTRIBUTE", + "=", "'{}'"), columns.get(3).getColumnSpecs()); + assertEquals(List.of(ConstraintKind.NOT_NULL), table.getConstraints().stream() + .map(ConstraintDeclaration::getKind).collect(Collectors.toList())); + } + } + + @Test + void typedSettersChangeBothRenderersAndLeaveSnapshotsDetached() throws JSQLParserException { + Statement statement = parse("CREATE TABLE t (g POINT SRID 4326, a INT COLUMN_FORMAT FIXED, " + + "c JSON SECONDARY_ENGINE_ATTRIBUTE '{}')", Dialect.MYSQL); + List columns = columns(statement); + ColumnOption srid = columns.get(0).getColumnOptions().get(0); + ColumnOption format = columns.get(1).getColumnOptions().get(0); + ColumnOption attribute = columns.get(2).getColumnOptions().get(0); + List snapshot = attribute.getTokens(); + assertReadOnly(snapshot); + + srid.setSrid(0); + format.setColumnFormat(ColumnFormat.DYNAMIC); + attribute.setEngineAttribute(new StringValue("{\"k\":2}")); + attribute.setUseEquals(true); + + assertEquals(List.of("SECONDARY_ENGINE_ATTRIBUTE", "'{}'"), snapshot); + assertEquals(List.of("SECONDARY_ENGINE_ATTRIBUTE", "=", "'{\"k\":2}'"), + attribute.getTokens()); + assertEquals("CREATE TABLE t (g POINT SRID 0, a INT COLUMN_FORMAT DYNAMIC, " + + "c JSON SECONDARY_ENGINE_ATTRIBUTE = '{\"k\":2}')", statement.toString()); + assertRoundTrip(statement); + assertThrows(IllegalStateException.class, () -> srid.setTokens(List.of("SRID", "1"))); + assertThrows(NullPointerException.class, () -> format.setColumnFormat(null)); + assertThrows(NullPointerException.class, () -> attribute.setEngineAttribute(null)); + } + + @Test + void engineAttributeLiteralsAreVisitedLikeComments() throws JSQLParserException { + Statement statement = parse( + "ALTER TABLE t ADD COLUMN c JSON COMMENT 'c' ENGINE_ATTRIBUTE '{}'", Dialect.MYSQL); + ColumnDefinition column = columns(statement).get(0); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(column, visited::add, ignored -> { + }); + assertEquals(2, visited.size()); + assertSame(column.getColumnOptions().get(1).getEngineAttribute(), visited.get(1)); + } + + @ParameterizedTest + @MethodSource("factories") + void factoriesRenderCanonicalSql(ColumnOption option, String sql, List tokens) { + assertEquals(sql, option.toString()); + assertEquals(tokens, option.getTokens()); + assertReadOnly(option.getTokens()); + assertThrows(IllegalStateException.class, () -> option.setTokens(List.of("raw"))); + } + + private static Stream factories() { + ColumnOption withEquals = ColumnOption.engineAttribute(new StringValue("{}")); + withEquals.setUseEquals(true); + return Stream.of( + new Object[] {ColumnOption.srid(4326), "SRID 4326", List.of("SRID", "4326")}, + new Object[] {ColumnOption.columnFormat(ColumnFormat.FIXED), "COLUMN_FORMAT FIXED", + List.of("COLUMN_FORMAT", "FIXED")}, + new Object[] {ColumnOption.engineAttribute(new StringValue("{}")), + "ENGINE_ATTRIBUTE '{}'", List.of("ENGINE_ATTRIBUTE", "'{}'")}, + new Object[] {withEquals, "ENGINE_ATTRIBUTE = '{}'", + List.of("ENGINE_ATTRIBUTE", "=", "'{}'")}, + new Object[] {ColumnOption.secondaryEngineAttribute(new StringValue("{}")), + "SECONDARY_ENGINE_ATTRIBUTE '{}'", + List.of("SECONDARY_ENGINE_ATTRIBUTE", "'{}'")}, + new Object[] { + ColumnOption.constraint(new KeyConstraint().withType("KEY") + .withKind(ConstraintKind.PRIMARY_KEY)), + "KEY", List.of("KEY")}, + new Object[] {ColumnOption.constraint(new KeyConstraint().withType("UNIQUE KEY")), + "UNIQUE KEY", List.of("UNIQUE", "KEY")}); + } + + @Test + void unsupportedColumnFormatsStayOpaque() throws JSQLParserException { + // Percona Server's compressed columns are not modeled. + Statement statement = + parse("CREATE TABLE t (c BLOB COLUMN_FORMAT COMPRESSED)", Dialect.MYSQL); + assertTrue(options(columns(statement).get(0)) + .allMatch(option -> option.getKind() == ColumnOption.Kind.OTHER)); + assertEquals("CREATE TABLE t (c BLOB COLUMN_FORMAT COMPRESSED)", statement.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (code VARCHAR(10) UNIQUE KEY NOT NULL)", + "ALTER TABLE t ADD COLUMN code VARCHAR(10) UNIQUE KEY NOT NULL"}) + void uniqueKeyIsOneConstraint(String sql) throws JSQLParserException { + for (Dialect dialect : DIALECTS) { + Statement statement = parse(sql, dialect); + ColumnDefinition column = columns(statement).get(0); + assertEquals(2, column.getColumnOptions().size()); + ColumnOption unique = column.getColumnOptions().get(0); + assertEquals(ColumnOption.Kind.CONSTRAINT, unique.getKind()); + assertEquals(ConstraintKind.UNIQUE, unique.getConstraintKind()); + assertEquals("UNIQUE KEY", unique.getConstraint().getType()); + assertEquals(List.of("UNIQUE", "KEY", "NOT", "NULL"), column.getColumnSpecs()); + assertRoundTrip(statement); + } + } + + @Test + void bareKeyIsAPrimaryKeyThatKeepsItsSpelling() throws JSQLParserException { + for (Dialect dialect : DIALECTS) { + CreateTable table = + (CreateTable) parse("CREATE TABLE t (id INT KEY, name TEXT)", dialect); + ColumnOption key = table.getColumnDefinitions().get(0).getColumnOptions().get(0); + KeyConstraint constraint = assertInstanceOf(KeyConstraint.class, key.getConstraint()); + assertEquals("KEY", constraint.getType()); + assertEquals(ConstraintKind.PRIMARY_KEY, constraint.getKind()); + assertEquals(ConstraintKind.PRIMARY_KEY, key.getConstraintKind()); + assertEquals(List.of("KEY"), key.getTokens()); + ConstraintDeclaration declaration = table.getConstraints().get(0); + assertEquals(ConstraintKind.PRIMARY_KEY, declaration.getKind()); + assertSame(key, declaration.getColumnOption()); + assertEquals("CREATE TABLE t (id INT KEY, name TEXT)", table.toString()); + assertRoundTrip(table); + + constraint.setType("PRIMARY KEY"); + assertEquals(ConstraintKind.PRIMARY_KEY, key.getConstraintKind()); + assertEquals("CREATE TABLE t (id INT PRIMARY KEY, name TEXT)", table.toString()); + } + } + + @Test + void inlineForeignKeyPrefixIsNotAPrimaryKey() throws JSQLParserException { + CreateTable table = (CreateTable) parse( + "CREATE TABLE t (user_id INT FOREIGN KEY REFERENCES ra_user(id))", null); + List options = table.getColumnDefinitions().get(0).getColumnOptions(); + assertEquals(List.of(ColumnOption.Kind.OTHER, ColumnOption.Kind.REFERENCE), + options.stream().map(ColumnOption::getKind).collect(Collectors.toList())); + assertEquals(List.of("FOREIGN", "KEY"), options.get(0).getTokens()); + assertEquals(List.of(ConstraintKind.FOREIGN_KEY), table.getConstraints().stream() + .map(ConstraintDeclaration::getKind).collect(Collectors.toList())); + assertRoundTrip(table); + } + + private static Consumer dialect(Dialect dialect) { + return parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }; + } + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, dialect(dialect)); + } + + private static List columns(Statement statement) { + if (statement instanceof CreateTable) { + return ((CreateTable) statement).getColumnDefinitions(); + } + return new ArrayList<>( + ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList()); + } + + private static Stream options(ColumnDefinition column) { + return column.getColumnOptions() == null ? Stream.empty() + : column.getColumnOptions().stream(); + } + + private static void assertReadOnly(List tokens) { + assertThrows(UnsupportedOperationException.class, () -> tokens.set(0, "changed")); + assertThrows(UnsupportedOperationException.class, () -> tokens.add("changed")); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString(), Dialect.MYSQL).toString()); + assertEquals(sql.toString(), parse(sql.toString(), null).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlCommentLiteralTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlCommentLiteralTest.java new file mode 100644 index 0000000000..f8ffcd23ce --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlCommentLiteralTest.java @@ -0,0 +1,366 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterExpressionTableOption; +import net.sf.jsqlparser.statement.alter.AlterOperation; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** MySQL table and partition COMMENT literals and the values a server stores for them. */ +class MySqlCommentLiteralTest { + + /** Literals with the comment MySQL 8.4 stores for them. */ + static Stream mySqlLiterals() { + return Stream.of( + Arguments.of("'users'", "users"), + Arguments.of("'사용자 테이블'", "사용자 테이블"), + Arguments.of("'it''s'", "it's"), + Arguments.of("'it\\'s'", "it's"), + Arguments.of("\"dq\"", "dq"), + Arguments.of("\"double \\\"quoted\\\" text\"", "double \"quoted\" text"), + Arguments.of("''", ""), + Arguments.of("'line1\\nline2'", "line1\nline2"), + Arguments.of("'a\\\\b'", "a\\b"), + Arguments.of("'tab\\there'", "tab\there"), + Arguments.of("'percent\\% underscore\\_'", "percent\\% underscore\\_"), + Arguments.of("'emoji 😀'", "emoji 😀"), + Arguments.of("'first line\nsecond line'", "first line\nsecond line")); + } + + /** Literals that read the same with and without a dialect. */ + static Stream portableLiterals() { + return Stream.of( + Arguments.of("'users'", "users"), + Arguments.of("'it''s'", "it's"), + Arguments.of("''", ""), + Arguments.of("'사용자 테이블'", "사용자 테이블"), + Arguments.of("'emoji 😀'", "emoji 😀")); + } + + @ParameterizedTest + @MethodSource("mySqlLiterals") + void tableCommentsDecodeToTheStoredValue(String literal, String stored) + throws JSQLParserException { + assertTableComments(literal, stored, Dialect.MYSQL); + } + + @ParameterizedTest + @MethodSource("portableLiterals") + void tableCommentsDecodeWithoutDialect(String literal, String stored) + throws JSQLParserException { + assertTableComments(literal, stored, null); + } + + private static void assertTableComments(String literal, String stored, Dialect dialect) + throws JSQLParserException { + String sql = "CREATE TABLE t (id INT) ENGINE = InnoDB COMMENT = " + literal + + " DEFAULT CHARSET = utf8mb4"; + CreateTable create = (CreateTable) parse(sql, dialect); + assertComment(create.getTableOptions().get(1), literal, stored); + assertEquals(List.of("ENGINE", "=", "InnoDB", "COMMENT", "=", literal, "DEFAULT", + "CHARSET", "=", "utf8mb4"), create.getTableOptionsStrings()); + assertEquals(sql, create.toString()); + assertRoundTrip(create, dialect); + + String ctas = "CREATE TABLE t (id INT) COMMENT " + literal + " AS SELECT 1 AS id"; + CreateTable query = (CreateTable) parse(ctas, dialect); + assertComment(query.getTableOptions().get(0), literal, stored); + assertEquals(List.of("COMMENT", literal), query.getTableOptionsStrings()); + assertEquals(ctas, query.toString()); + assertRoundTrip(query, dialect); + + Alter alter = (Alter) parse("ALTER TABLE t COMMENT = " + literal, dialect); + AlterExpressionTableOption action = + (AlterExpressionTableOption) alter.getAlterExpressions().get(0); + assertComment(action.getStructuredTableOption(), literal, stored); + assertEquals(literal, action.getCommentText()); + assertEquals(AlterOperation.COMMENT_WITH_EQUAL_SIGN, action.getOperation()); + assertEquals("ALTER TABLE t COMMENT = " + literal, alter.toString()); + assertRoundTrip(alter, dialect); + } + + private static void assertComment(TableOption option, String literal, String stored) { + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertEquals(stored, option.getComment().getNotExcapedValue()); + assertEquals(literal, option.getComment().toString()); + assertEquals(literal, option.getValue()); + assertEquals(literal, option.getTokens().get(option.getTokens().size() - 1)); + } + + @Test + void mySqlEscapingIsRecordedOnTheLiteral() throws JSQLParserException { + CreateTable mySql = + (CreateTable) parse("CREATE TABLE t (id INT) COMMENT 'a\\\\b'", Dialect.MYSQL); + assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, + mySql.getTableOptions().get(0).getComment().getEscaping()); + CreateTable standard = (CreateTable) parse("CREATE TABLE t (id INT) COMMENT 'a\\b'", null); + assertEquals(StringValue.Escaping.STANDARD, + standard.getTableOptions().get(0).getComment().getEscaping()); + assertEquals("a\\b", standard.getTableOptions().get(0).getComment().getNotExcapedValue()); + } + + @Test + void alterCommentsKeepTheirPositionAmongOtherActions() throws JSQLParserException { + Alter alter = (Alter) parse( + "ALTER TABLE t MODIFY id INT COMMENT 'col', ENGINE=InnoDB, comment='it\\'s'", + Dialect.MYSQL); + List actions = alter.getAlterExpressions(); + TableOption comment = + ((AlterExpressionTableOption) actions.get(2)).getStructuredTableOption(); + assertEquals("comment", comment.getName()); + assertTrue(comment.isUseEquals()); + assertEquals("it's", comment.getComment().getNotExcapedValue()); + assertNull(((AlterExpressionTableOption) actions.get(1)).getStructuredTableOption() + .getComment()); + assertRoundTrip(alter, Dialect.MYSQL); + + Alter algorithm = + (Alter) parse("ALTER TABLE t COMMENT 'x', ALGORITHM=INSTANT", Dialect.MYSQL); + AlterExpressionTableOption action = + (AlterExpressionTableOption) algorithm.getAlterExpressions().get(0); + assertEquals(AlterOperation.COMMENT, action.getOperation()); + assertEquals("x", action.getStructuredTableOption().getComment().getNotExcapedValue()); + assertRoundTrip(algorithm, Dialect.MYSQL); + } + + @Test + void valuesThatAreNotStringLiteralsRemainPlainValues() throws JSQLParserException { + // Accepted leniently; without the MySQL dialect "x" is a quoted identifier + for (String sql : new String[] {"CREATE TABLE t (id INT) COMMENT = \"x\"", + "CREATE TABLE t (id INT) COMMENT plain"}) { + CreateTable create = (CreateTable) parse(sql, null); + TableOption option = create.getTableOptions().get(0); + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertNull(option.getComment()); + assertEquals(sql.substring(sql.lastIndexOf(' ') + 1), option.getValue()); + assertRoundTrip(create, null); + } + CreateTable mySql = + (CreateTable) parse("CREATE TABLE t (id INT) COMMENT = \"x\"", Dialect.MYSQL); + assertEquals("x", mySql.getTableOptions().get(0).getComment().getNotExcapedValue()); + } + + @Test + void commentLiteralEditsAreRenderedAndVisited() throws JSQLParserException { + CreateTable create = + (CreateTable) parse("CREATE TABLE t (id INT) comment = 'old'", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + StringValue replacement = new StringValue("'new'"); + option.setComment(replacement); + assertSame(replacement, option.getComment()); + assertEquals("comment = 'new'", option.toString()); + replacement.setValue("it''s"); + assertEquals("CREATE TABLE t (id INT) comment = 'it''s'", create.toString()); + assertEquals(List.of("comment", "=", "'it''s'"), option.getTokens()); + assertRoundTrip(create, Dialect.MYSQL); + + StringBuilder customized = new StringBuilder(); + option.appendTo(customized, expression -> customized.append("'printed'")); + assertEquals("comment = 'printed'", customized.toString()); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(option, visited::add, table -> { + }); + assertEquals(List.of(replacement), visited); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(create)); + } + + @Test + void commentFactoryAndReplacementOfOtherOptions() throws JSQLParserException { + TableOption created = TableOption.comment(new StringValue("'x'")); + assertEquals(TableOption.Kind.COMMENT, created.getKind()); + assertEquals("COMMENT", created.getName()); + assertFalse(created.isUseEquals()); + assertEquals("COMMENT 'x'", created.toString()); + assertEquals("COMMENT = 'x'", created.withUseEquals(true).toString()); + + CreateTable create = + (CreateTable) parse("CREATE TABLE t (id INT) ENGINE=InnoDB", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + option.withComment(new StringValue("'x'")); + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertEquals("COMMENT", option.getName()); + assertTrue(option.isUseEquals()); + assertEquals("CREATE TABLE t (id INT) COMMENT = 'x'", create.toString()); + assertTrue(create.getTableOption(TableOption.Kind.ENGINE).isEmpty()); + assertRoundTrip(create, Dialect.MYSQL); + + option.setComment(null); + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertNull(option.getValue()); + assertEquals("COMMENT", option.toString()); + } + + @Test + void scalarAndRawSettersReplaceTheLiteralWithoutParsing() throws JSQLParserException { + CreateTable create = + (CreateTable) parse("CREATE TABLE t (id INT) COMMENT='it\\'s'", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + option.setValue("'plain'"); + assertNull(option.getComment()); + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertEquals("COMMENT = 'plain'", option.toString()); + assertRoundTrip(create, Dialect.MYSQL); + + option.setComment(new StringValue("'typed'")); + option.setKind(TableOption.Kind.OTHER); + assertNull(option.getComment()); + assertEquals("'typed'", option.getValue()); + option.setKind(TableOption.Kind.COMMENT); + assertNull(option.getComment()); + assertEquals("COMMENT = 'typed'", option.toString()); + + option.setComment(new StringValue("'typed'")); + List raw = new ArrayList<>(List.of("COMMENT", "'raw'")); + option.setTokens(raw); + assertNull(option.getComment()); + assertEquals(TableOption.Kind.OTHER, option.getKind()); + assertSame(raw, option.getTokens()); + option.setTokens(null); + assertNull(option.getComment()); + + option.setTokens(raw); + option.setComment(new StringValue("'typed'")); + assertEquals(List.of("COMMENT", "'typed'"), option.getTokens()); + assertRoundTrip(create, Dialect.MYSQL); + + AlterExpressionTableOption action = (AlterExpressionTableOption) ((Alter) parse( + "ALTER TABLE t COMMENT 'old'", Dialect.MYSQL)).getAlterExpressions().get(0); + action.setCommentText("'new'"); + assertNull(action.getStructuredTableOption().getComment()); + assertEquals("'new'", action.getCommentText()); + } + + @Test + void partitionAndSubpartitionCommentsDecodeToTheStoredValue() throws JSQLParserException { + String sql = "CREATE TABLE t (id INT, d INT) PARTITION BY RANGE (id) " + + "SUBPARTITION BY HASH (d) (PARTITION p0 VALUES LESS THAN (10) " + + "COMMENT = 'it\\'s' (SUBPARTITION s0 COMMENT 'sub0', " + + "SUBPARTITION s1 COMMENT = 'line1\\nline2'), " + + "PARTITION p1 VALUES LESS THAN MAXVALUE COMMENT '사용자 😀' " + + "(SUBPARTITION s2, SUBPARTITION s3 COMMENT 'a\\\\b'))"; + CreateTable create = (CreateTable) parse(sql, Dialect.MYSQL); + List partitions = + create.getPartitioning().getPartitionDefinitions(); + assertPartitionComment(partitions.get(0), "'it\\'s'", "it's", true); + assertPartitionComment(partitions.get(0).getSubPartitionDefinitions().get(0), "'sub0'", + "sub0", false); + assertPartitionComment(partitions.get(0).getSubPartitionDefinitions().get(1), + "'line1\\nline2'", "line1\nline2", true); + assertPartitionComment(partitions.get(1), "'사용자 😀'", "사용자 😀", false); + assertNull(partitions.get(1).getSubPartitionDefinitions().get(0).getCommentValue()); + assertNull(partitions.get(1).getSubPartitionDefinitions().get(0).getComment()); + assertPartitionComment(partitions.get(1).getSubPartitionDefinitions().get(1), "'a\\\\b'", + "a\\b", false); + assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, + partitions.get(0).getCommentValue().getEscaping()); + assertEquals(sql, create.toString()); + assertRoundTrip(create, Dialect.MYSQL); + } + + @Test + void alterPartitionCommentsDecodeToTheStoredValue() throws JSQLParserException { + for (Dialect dialect : new Dialect[] {Dialect.MYSQL, null}) { + Alter add = (Alter) parse("ALTER TABLE t ADD PARTITION (PARTITION p1 VALUES " + + "LESS THAN (20) COMMENT 'it''s added')", dialect); + PartitionDefinition added = + add.getAlterExpressions().get(0).getPartitionDefinitions().get(0); + assertPartitionComment(added, "'it''s added'", "it's added", false); + assertRoundTrip(add, dialect); + + Alter reorganize = (Alter) parse("ALTER TABLE t REORGANIZE PARTITION p1 INTO " + + "(PARTITION p1a VALUES LESS THAN (15) COMMENT = 'reorg', " + + "PARTITION p1b VALUES LESS THAN (20))", dialect); + List definitions = + reorganize.getAlterExpressions().get(0).getPartitionDefinitions(); + assertPartitionComment(definitions.get(0), "'reorg'", "reorg", true); + assertNull(definitions.get(1).getCommentValue()); + assertRoundTrip(reorganize, dialect); + } + } + + private static void assertPartitionComment(PartitionDefinition definition, String literal, + String stored, boolean useEquals) { + assertEquals(stored, definition.getCommentValue().getNotExcapedValue()); + assertEquals(literal, definition.getComment()); + assertEquals(useEquals, definition.isCommentUseEquals()); + } + + @Test + void partitionCommentStringAccessorsConvertTheLiteral() throws JSQLParserException { + CreateTable create = (CreateTable) parse("CREATE TABLE t (id INT) PARTITION BY " + + "RANGE (id) (PARTITION p0 VALUES LESS THAN (10) COMMENT 'old')", Dialect.MYSQL); + PartitionDefinition definition = create.getPartitioning().getPartitionDefinitions().get(0); + + definition.setComment("'it''s'"); + assertInstanceOf(StringValue.class, definition.getCommentValue()); + assertEquals(StringValue.Escaping.STANDARD, definition.getCommentValue().getEscaping()); + assertEquals("it's", definition.getCommentValue().getNotExcapedValue()); + assertEquals("'it''s'", definition.getComment()); + assertRoundTrip(create, Dialect.MYSQL); + + StringValue escaped = new StringValue("'a\\nb'") + .withEscaping(StringValue.Escaping.MYSQL_BACKSLASH); + definition.setCommentValue(escaped); + definition.setCommentUseEquals(true); + assertSame(escaped, definition.getCommentValue()); + assertEquals("'a\\nb'", definition.getComment()); + assertEquals("PARTITION p0 VALUES LESS THAN (10) COMMENT = 'a\\nb'", + definition.toString()); + assertRoundTrip(create, Dialect.MYSQL); + + definition.setComment(null); + assertNull(definition.getCommentValue()); + assertEquals("PARTITION p0 VALUES LESS THAN (10)", definition.toString()); + } + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + } + + private static void assertRoundTrip(Statement statement, Dialect dialect) + throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlCreateTableDuplicatesTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlCreateTableDuplicatesTest.java new file mode 100644 index 0000000000..f6bdff70c4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlCreateTableDuplicatesTest.java @@ -0,0 +1,76 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.CreateTable.DuplicateHandling; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlCreateTableDuplicatesTest { + @ParameterizedTest + @ValueSource(strings = {"SELECT id FROM source", "TABLE source", "VALUES ROW(1), ROW(2)", + "(SELECT id FROM source)", "WITH c AS (SELECT 1 AS id) SELECT id FROM c"}) + void retainsModifierAndSourceAfterTableOptions(String query) throws JSQLParserException { + for (DuplicateHandling handling : DuplicateHandling.values()) { + for (boolean useAs : new boolean[] {false, true}) { + CreateTable table = parse("CREATE TABLE t (id INT PRIMARY KEY) ENGINE=InnoDB " + + handling + (useAs ? " AS " : " ") + query); + assertEquals(handling, table.getDuplicateHandling()); + assertEquals(useAs, table.isUseAsKeyword()); + assertNotNull(table.getSelect()); + roundTrip(table); + } + } + } + + @Test + void preservesPartitionAndAllowsAstMutation() throws JSQLParserException { + CreateTable table = parse("CREATE TABLE t (id INT PRIMARY KEY) PARTITION BY HASH(id) " + + "PARTITIONS 2 IGNORE AS SELECT id FROM source"); + assertNotNull(table.getPartitioning()); + assertNull(table.getPartitioning().getPartitionOptions()); + table.setDuplicateHandling(DuplicateHandling.REPLACE); + assertTrue(table.toString().contains(" REPLACE AS SELECT")); + roundTrip(table); + table.setDuplicateHandling(null); + assertFalse(table.toString().contains("REPLACE")); + roundTrip(table); + assertNull(parse("CREATE TABLE t AS SELECT 1").getDuplicateHandling()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t IGNORE", "CREATE TABLE t REPLACE AS", + "CREATE TABLE t IGNORE REPLACE SELECT 1", + "CREATE TABLE t ENGINE=InnoDB, IGNORE SELECT 1"}) + void rejectsMissingQueriesAndConflictingModifiers(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static void roundTrip(CreateTable table) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + table.accept(new StatementDeParser(out)); + assertEquals(table.toString(), out.toString()); + CreateTable parsed = parse(out.toString()); + assertEquals(table.getDuplicateHandling(), parsed.getDuplicateHandling()); + assertEquals(table.isUseAsKeyword(), parsed.isUseAsKeyword()); + assertEquals(out.toString(), parsed.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlCreateTableSourceTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlCreateTableSourceTest.java new file mode 100644 index 0000000000..34968a3077 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlCreateTableSourceTest.java @@ -0,0 +1,77 @@ +/*- + * #%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.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.select.TableStatement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlCreateTableSourceTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t AS TABLE db.s", "CREATE TABLE t TABLE db.s", + "CREATE TABLE t (id BIGINT) TABLE db.s", + "CREATE TEMPORARY TABLE IF NOT EXISTS t ENGINE=InnoDB TABLE `db`.`s`", + "CREATE TABLE t (extra INT) ENGINE=InnoDB, COMMENT='source' AS TABLE s ORDER BY id DESC LIMIT 2 OFFSET 1", + "CREATE TABLE t AS SELECT 1 AS id", "CREATE TABLE t SELECT 1 AS id", + "CREATE TABLE t AS (SELECT 1 AS id)"}) + void roundTripsQuerySources(String sql) throws Exception { + CreateTable table = parse(sql); + assertNotNull(table.getSelect()); + if (sql.contains("TABLE db.") || sql.contains("TABLE `db`")) { + assertInstanceOf(TableStatement.class, table.getSelect()); + } + assertRoundTrip(table); + } + + @Test + void retainsSourceStructureAndAsChoiceAfterMutation() throws Exception { + CreateTable table = parse("CREATE TABLE target TABLE old.source"); + assertFalse(table.isUseAsKeyword()); + TableStatement source = assertInstanceOf(TableStatement.class, table.getSelect()); + source.getTable().setSchemaName("newdb"); + assertThat(new TablesNamesFinder().getTables(table)) + .containsExactlyInAnyOrder("target", "newdb.source"); + assertEquals("CREATE TABLE target TABLE newdb.source", table.toString()); + table.setUseAsKeyword(true); + assertEquals("CREATE TABLE target AS TABLE newdb.source", table.toString()); + assertRoundTrip(table); + assertTrue(new CreateTable().isUseAsKeyword()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t AS TABLE", "CREATE TABLE t TABLE", + "CREATE TABLE t ENGINE=InnoDB, TABLE s", "CREATE TABLE t AS"}) + void rejectsMissingSourcesAndDanglingOptionSeparators(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static void assertRoundTrip(CreateTable table) throws Exception { + StringBuilder output = new StringBuilder(); + table.accept(new StatementDeParser(output), null); + assertEquals(table.toString(), output.toString()); + CreateTable reparsed = parse(output.toString()); + assertEquals(table.toString(), reparsed.toString()); + assertEquals(table.isUseAsKeyword(), reparsed.isUseAsKeyword()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlIndexSpecOptionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlIndexSpecOptionTest.java new file mode 100644 index 0000000000..4ea4f93b1b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlIndexSpecOptionTest.java @@ -0,0 +1,421 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.create.index.AlgorithmOrLockOption; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.IndexOptionSource; +import net.sf.jsqlparser.statement.create.table.IndexSpecOption; +import net.sf.jsqlparser.statement.create.table.IndexSpecOption.Kind; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlIndexSpecOptionTest { + private static final String OPTIONS = "KEY_BLOCK_SIZE = 8 USING HASH WITH PARSER ngram " + + "COMMENT 'c' INVISIBLE ENGINE_ATTRIBUTE '{}' SECONDARY_ENGINE_ATTRIBUTE = '{}'"; + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void createIndexKeepsIndexOptionsOnItsIndexAndAlgorithmLockOnTheStatement(boolean mysql) + throws Exception { + String sql = "CREATE UNIQUE INDEX uk ON t1 (a DESC, b (10)) USING BTREE COMMENT 'x' " + + "ALGORITHM = INPLACE LOCK = NONE"; + CreateIndex create = (CreateIndex) roundTrip(sql, mysql); + Index index = create.getIndex(); + + assertNull(index.getUsing()); + assertOptions(index, "INDEX_TYPE BTREE", "COMMENT 'x'"); + assertEquals(2, create.getAlgorithmOrLockOptions().size()); + assertAlgorithmOrLock(create.getAlgorithmOrLockOptions().get(0), + AlgorithmOrLockOption.Kind.ALGORITHM, "INPLACE"); + assertAlgorithmOrLock(create.getAlgorithmOrLockOptions().get(1), + AlgorithmOrLockOption.Kind.LOCK, "NONE"); + assertEquals(List.of("USING", "BTREE", "COMMENT", "'x'"), index.getIndexSpec()); + assertEquals(List.of("USING", "BTREE", "COMMENT", "'x'", "ALGORITHM", "=", "INPLACE", + "LOCK", "=", "NONE"), create.getTailParameters()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "CREATE FULLTEXT INDEX ft ON t1 (body) WITH PARSER ngram|PARSER ngram", + "CREATE INDEX i ON t1 (a) INVISIBLE|VISIBILITY false", + "CREATE INDEX i ON t1 (a) VISIBLE|VISIBILITY true", + "CREATE INDEX i ON db1.t1 (a, b) KEY_BLOCK_SIZE = 8|KEY_BLOCK_SIZE 8 =", + "CREATE INDEX i ON t1 (a) KEY_BLOCK_SIZE 8 ENGINE_ATTRIBUTE '{}'" + + "|KEY_BLOCK_SIZE 8,ENGINE_ATTRIBUTE '{}'"}) + void createIndexOptionsAreTyped(String sql, String expected) throws Exception { + for (boolean mysql : List.of(true, false)) { + CreateIndex create = (CreateIndex) roundTrip(sql, mysql); + assertOptions(create.getIndex(), expected.split(",")); + assertTrue(create.getAlgorithmOrLockOptions().isEmpty()); + } + } + + @Test + void indexTypeKeepsItsPositionRelativeToTheKeyList() throws Exception { + CreateIndex beforeOn = (CreateIndex) roundTrip("CREATE INDEX i USING HASH ON t1 (a)", true); + assertEquals("HASH", beforeOn.getIndex().getUsing()); + assertTrue(beforeOn.isIndexTypeBeforeOn()); + assertNull(beforeOn.getIndex().getSpecOptions()); + + CreateTable table = (CreateTable) roundTrip("CREATE TABLE t20 (a INT, b VARCHAR (100), " + + "KEY idx_a USING BTREE (a) KEY_BLOCK_SIZE = 4 COMMENT 'idx', " + + "UNIQUE INDEX uk_b (b (20)) USING HASH)", true); + Index key = table.getIndexes().get(0); + assertEquals("BTREE", key.getUsing()); + assertOptions(key, "KEY_BLOCK_SIZE 4 =", "COMMENT 'idx'"); + assertEquals(List.of("KEY_BLOCK_SIZE", "=", "4", "COMMENT", "'idx'"), key.getIndexSpec()); + KeyConstraint unique = (KeyConstraint) table.getTableConstraints().get(0); + assertNull(unique.getUsing()); + assertOptions(unique, "INDEX_TYPE HASH"); + assertEquals(List.of("USING", "HASH"), unique.getIndexSpec()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t1 ADD CONSTRAINT pk_t1 PRIMARY KEY USING BTREE (id)", + "ALTER TABLE t1 ADD PRIMARY KEY USING BTREE (id)", + "ALTER TABLE t1 ADD CONSTRAINT uk UNIQUE KEY uk_idx USING BTREE (id)", + "CREATE TABLE t1 (id INT, PRIMARY KEY USING BTREE (id) COMMENT 'pk')", + "CREATE TABLE t1 (id INT, CONSTRAINT pk_t1 PRIMARY KEY USING HASH (id))"}) + void keysAcceptTheIndexTypeBeforeTheKeyList(String sql) throws Exception { + KeyConstraint key = keyConstraint(roundTrip(sql, true)); + assertEquals(sql.contains("HASH") ? "HASH" : "BTREE", key.getUsing()); + assertEquals(List.of("id"), key.getColumnsNames()); + assertFalse(key.getSpecOptions().stream() + .anyMatch(option -> option.getKind() == Kind.INDEX_TYPE)); + } + + @Test + void defaultModeAcceptsTheIndexTypeBeforeNamedConstraintKeys() throws Exception { + for (String sql : List.of( + "ALTER TABLE t1 ADD CONSTRAINT pk_t1 PRIMARY KEY USING BTREE (id)", + "ALTER TABLE t1 ADD CONSTRAINT uk UNIQUE KEY USING BTREE (id)")) { + Statement statement = roundTrip(sql, false); + KeyConstraint key = keyConstraint(statement); + assertEquals("BTREE", key.getUsing()); + key.setUsing("HASH"); + assertRendered(statement, sql.replace("BTREE", "HASH")); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE INDEX ix ON t (a) %s", + "CREATE TABLE t (a INT, KEY ix (a) %s)", + "CREATE TABLE t (a INT, UNIQUE KEY ix (a) %s)", + "ALTER TABLE t ADD INDEX ix (a) %s", "ALTER TABLE t ADD UNIQUE KEY ix (a) %s", + "ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (a) %s"}) + void everyDefinitionSharesTheSameOptionModel(String template) throws Exception { + IndexOptionSource source = definition(roundTrip(String.format(template, OPTIONS), true)); + assertOptions(source, "KEY_BLOCK_SIZE 8 =", "INDEX_TYPE HASH", "PARSER ngram", + "COMMENT 'c'", "VISIBILITY false", "ENGINE_ATTRIBUTE '{}'", + "SECONDARY_ENGINE_ATTRIBUTE '{}' ="); + assertEquals(List.of("KEY_BLOCK_SIZE", "=", "8", "USING", "HASH", "WITH", "PARSER", + "ngram", "COMMENT", "'c'", "INVISIBLE", "ENGINE_ATTRIBUTE", "'{}'", + "SECONDARY_ENGINE_ATTRIBUTE", "=", "'{}'"), source.getIndexSpec()); + } + + @Test + void alterKeyBlockSizeWithoutEqualsKeepsItsSeparator() throws Exception { + // ALTER used to render this option as KEY_BLOCK_SIZE8 + Alter alter = (Alter) roundTrip("ALTER TABLE t ADD INDEX i (a) KEY_BLOCK_SIZE 8", true); + assertOptions(alter.getAlterExpressions().get(0).getIndex(), "KEY_BLOCK_SIZE 8"); + } + + @Test + void commentsAreStringValuesThatDecodeTheStoredText() throws Exception { + String[][] comments = {{"'it''s'", "it's"}, {"'it\\'s'", "it's"}, {"\"dq\"", "dq"}, + {"'a\\\\b'", "a\\b"}, {"'인덱스 설명'", "인덱스 설명"}}; + for (String template : List.of("CREATE INDEX ix ON t (a) COMMENT %s", + "CREATE TABLE t (a INT, KEY ix (a) COMMENT %s)", + "ALTER TABLE t ADD INDEX ix (a) COMMENT %s")) { + for (String[] comment : comments) { + String sql = String.format(template, comment[0]); + IndexSpecOption option = + definition(roundTrip(sql, true)).getSpecOptions().get(0); + assertEquals(Kind.COMMENT, option.getKind()); + assertEquals(comment[0], option.getValue(), sql); + assertEquals(comment[0], option.getComment().toString(), sql); + assertEquals(comment[1], option.getComment().getNotExcapedValue(), sql); + } + } + + Alter alter = (Alter) roundTrip("ALTER TABLE t ADD INDEX ix (a) COMMENT 'it\\'s'", true); + IndexSpecOption comment = alter.getAlterExpressions().get(0).getIndex() + .getSpecOptions().get(0); + assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, comment.getComment().getEscaping()); + comment.setValue("'don\\'t'"); + assertEquals("don't", comment.getComment().getNotExcapedValue()); + comment.setComment(new StringValue("'changed'")); + assertRendered(alter, "ALTER TABLE t ADD INDEX ix (a) COMMENT 'changed'"); + assertEquals(List.of("COMMENT", "'changed'"), + alter.getAlterExpressions().get(0).getIndex().getIndexSpec()); + } + + @Test + void typedEditsReachBothRenderers() throws Exception { + Alter alter = (Alter) roundTrip( + "ALTER TABLE t1 ADD INDEX i3 (a) USING BTREE COMMENT 'c' INVISIBLE", true); + List options = alter.getAlterExpressions().get(0).getIndex() + .getSpecOptions(); + options.get(0).setValue("HASH"); + options.get(1).setValue("'changed'"); + options.get(2).setVisible(true); + options.add(0, IndexSpecOption.of(Kind.KEY_BLOCK_SIZE, "16", true)); + assertRendered(alter, "ALTER TABLE t1 ADD INDEX i3 (a) KEY_BLOCK_SIZE = 16 USING HASH " + + "COMMENT 'changed' VISIBLE"); + + CreateIndex create = (CreateIndex) roundTrip( + "CREATE INDEX i ON t (a) LOCK EXCLUSIVE ALGORITHM DEFAULT", true); + create.getAlgorithmOrLockOptions().get(0).setValue("SHARED"); + create.getAlgorithmOrLockOptions().get(1).setUseEquals(true); + create.getIndex().getIndexOptions().addSpecOptions(IndexSpecOption.of(Kind.COMMENT, "'x'")); + assertRendered(create, + "CREATE INDEX i ON t (a) COMMENT 'x' LOCK SHARED ALGORITHM = DEFAULT"); + assertEquals(List.of("COMMENT", "'x'", "LOCK", "SHARED", "ALGORITHM", "=", "DEFAULT"), + create.getTailParameters()); + } + + @Test + void legacyAccessorsAreSnapshotsAndSettersReplaceWithOpaqueOptions() throws Exception { + CreateIndex create = (CreateIndex) roundTrip( + "CREATE INDEX i ON t (a) parallel compress nologging", false); + assertEquals(List.of("parallel", "compress", "nologging"), create.getTailParameters()); + assertTrue(create.getIndex().getSpecOptions().stream() + .allMatch(option -> option.getKind() == Kind.OTHER)); + assertThrows(UnsupportedOperationException.class, + () -> create.getTailParameters().add("x")); + assertThrows(UnsupportedOperationException.class, + () -> create.getIndex().getIndexSpec().add("x")); + + create.addAlgorithmOrLockOptions(AlgorithmOrLockOption.lock("NONE")); + List tail = new ArrayList<>(List.of("KEY_BLOCK_SIZE", "=", "4")); + create.setTailParameters(tail); + tail.clear(); + assertTrue(create.getAlgorithmOrLockOptions().isEmpty()); + assertEquals(Kind.OTHER, create.getIndex().getSpecOptions().get(0).getKind()); + assertRendered(create, "CREATE INDEX i ON t (a) KEY_BLOCK_SIZE = 4"); + + Index index = new Index().withName("i").withColumnsNames(List.of("a")); + assertNull(index.getIndexSpec()); + index.setIndexSpec(List.of("USING", "BTREE")); + assertEquals(List.of("USING", "BTREE"), index.getIndexSpec()); + assertEquals("i (a) USING BTREE", index.toString()); + index.setIndexSpec(List.of()); + assertTrue(index.getSpecOptions().isEmpty()); + index.setIndexSpec(null); + assertNull(index.getSpecOptions()); + assertEquals(List.of(), new CreateIndex().getTailParameters()); + } + + @Test + void typedOptionsRejectPayloadsOfOtherKinds() { + IndexSpecOption comment = IndexSpecOption.comment(new StringValue("'c'")); + assertEquals("'c'", comment.getValue()); + assertThrows(IllegalStateException.class, + () -> IndexSpecOption.of(Kind.PARSER, "ngram").setComment(new StringValue("'x'"))); + assertNull(IndexSpecOption.of(Kind.PARSER, "ngram").getComment()); + assertThrows(NullPointerException.class, () -> comment.setComment(null)); + assertThrows(IllegalStateException.class, () -> comment.setTokens(List.of("x"))); + assertThrows(IllegalStateException.class, () -> comment.setUseEquals(true)); + assertThrows(IllegalStateException.class, () -> comment.setVisible(true)); + assertNull(comment.getVisible()); + IndexSpecOption visible = IndexSpecOption.visibility(true); + assertThrows(IllegalStateException.class, () -> visible.setValue("x")); + assertEquals(List.of("VISIBLE"), visible.getTokens()); + assertThrows(IllegalStateException.class, + () -> IndexSpecOption.of(Kind.OTHER, "x")); + assertThrows(NullPointerException.class, () -> IndexSpecOption.of(Kind.PARSER, null)); + + List tokens = new ArrayList<>(List.of("ON", "[PRIMARY]")); + IndexSpecOption raw = IndexSpecOption.raw(tokens); + tokens.set(1, "ignored"); + assertEquals("ON [PRIMARY]", raw.toString()); + assertThrows(UnsupportedOperationException.class, () -> raw.getTokens().add("x")); + assertThrows(IllegalStateException.class, () -> raw.setValue("x")); + raw.setTokens(List.of("ON", "fg")); + assertEquals(List.of("ON", "fg"), raw.getTokens()); + } + + @Test + void dropIndexAlgorithmAndLockAreTyped() throws Exception { + Drop drop = (Drop) roundTrip("DROP INDEX idx ON t1 ALGORITHM = INPLACE LOCK = NONE", true); + assertEquals("t1", drop.getTable().getName()); + assertAlgorithmOrLock(drop.getAlgorithmOrLockOptions().get(0), + AlgorithmOrLockOption.Kind.ALGORITHM, "INPLACE"); + assertAlgorithmOrLock(drop.getAlgorithmOrLockOptions().get(1), + AlgorithmOrLockOption.Kind.LOCK, "NONE"); + assertEquals(List.of("ON", "t1", "ALGORITHM", "=", "INPLACE", "LOCK", "=", "NONE"), + drop.getParameters()); + + drop.getAlgorithmOrLockOptions().remove(1); + drop.getAlgorithmOrLockOptions().get(0).setValue("COPY"); + drop.setBehavior(CascadeBehavior.RESTRICT); + assertRendered(drop, "DROP INDEX idx ON t1 ALGORITHM = COPY RESTRICT"); + assertEquals(List.of("ON", "t1", "ALGORITHM", "=", "COPY", "RESTRICT"), + drop.getParameters()); + + drop.addParameters("legacy"); + drop.setAlgorithmOrLockOptions(List.of(AlgorithmOrLockOption.lock("SHARED"))); + assertRendered(drop, "DROP INDEX idx ON t1 legacy LOCK = SHARED RESTRICT"); + drop.setAlgorithmOrLockOptions(List.of(AlgorithmOrLockOption.lock("SHARED")), 0); + assertEquals(List.of("ON", "t1", "LOCK", "=", "SHARED", "legacy", "RESTRICT"), + drop.getParameters()); + assertThrows(IllegalArgumentException.class, + () -> drop.setAlgorithmOrLockOptions(List.of(), 2)); + + drop.setParameters(List.of("CASCADE")); + assertTrue(drop.getAlgorithmOrLockOptions().isEmpty()); + assertNull(drop.getBehavior()); + assertNull(drop.getTable()); + assertEquals("DROP INDEX idx CASCADE", drop.toString()); + + Drop single = (Drop) roundTrip("DROP INDEX idx ON t1 ALGORITHM = INPLACE", false); + assertEquals(1, single.getAlgorithmOrLockOptions().size()); + } + + @Test + void replacingTheCreateIndexDefinitionKeepsOrInheritsItsOptions() throws Exception { + CreateIndex create = (CreateIndex) roundTrip( + "CREATE INDEX i ON t (a) KEY_BLOCK_SIZE = 8", true); + Index removed = create.getIndex(); + IndexSpecOption keyBlockSize = removed.getSpecOptions().get(0); + create.setIndex(null); + assertEquals(List.of("KEY_BLOCK_SIZE", "=", "8"), create.getTailParameters()); + + create.setIndex(new Index().withName("j").withColumnsNames(List.of("b"))); + assertSame(keyBlockSize, create.getIndex().getSpecOptions().get(0)); + assertNotSame(removed.getSpecOptions(), create.getIndex().getSpecOptions()); + assertRendered(create, "CREATE INDEX j ON t (b) KEY_BLOCK_SIZE = 8"); + + create.setIndex(new Index().withName("k").withColumnsNames(List.of("c")) + .withSpecOptions(List.of(IndexSpecOption.visibility(false)))); + assertRendered(create, "CREATE INDEX k ON t (c) INVISIBLE"); + } + + @Test + void postgreSqlIndexOptionsKeepTheirProperties() throws Exception { + String sql = "CREATE UNIQUE INDEX CONCURRENTLY ix ON ONLY t USING btree (id) " + + "INCLUDE (v) NULLS NOT DISTINCT WITH (fillfactor = 80) TABLESPACE fast " + + "WHERE active"; + CreateIndex create = (CreateIndex) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(sql, create.toString()); + assertEquals("btree", create.getIndex().getUsing()); + assertEquals(List.of("v"), create.getIncludeColumns()); + assertEquals("fast", create.getTableSpace()); + assertNull(create.getIndex().getSpecOptions()); + assertTrue(create.getTailParameters().isEmpty()); + + Alter alter = (Alter) CCJSqlParserUtil.parse("ALTER TABLE t ADD CONSTRAINT uk UNIQUE (id) " + + "INCLUDE (v) WITH (fillfactor = 70) USING INDEX TABLESPACE fast", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + KeyConstraint key = alter.getAlterExpressions().get(0).getKeyConstraint(); + assertEquals("fast", key.getTableSpace()); + assertEquals(1, key.getStorageParameters().size()); + assertTrue(key.getSpecOptions().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ADD PRIMARY KEY USING (id)", + "ALTER TABLE t ADD PRIMARY KEY USING BTREE USING HASH (id)", + "ALTER TABLE t ADD INDEX i (a) KEY_BLOCK_SIZE", + "ALTER TABLE t ADD INDEX i (a) WITH PARSER", + "ALTER TABLE t ADD INDEX i (a) COMMENT", + "CREATE INDEX i ON t (a) LOCK = NONE ALGORITHM"}) + void rejectsIncompleteOptions(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } + + private static void assertOptions(IndexOptionSource source, String... expected) { + List actual = new ArrayList<>(); + for (IndexSpecOption option : source.getSpecOptions()) { + Object payload = option.getKind() == Kind.VISIBILITY ? option.getVisible() + : option.getValue(); + actual.add(option.getKind() + " " + payload + (option.isUseEquals() ? " =" : "")); + } + assertEquals(List.of(expected), actual); + } + + private static void assertAlgorithmOrLock(AlgorithmOrLockOption option, + AlgorithmOrLockOption.Kind kind, String value) { + assertEquals(kind, option.getKind()); + assertEquals(value, option.getValue()); + assertTrue(option.isUseEquals()); + } + + private static IndexOptionSource definition(Statement statement) { + if (statement instanceof CreateIndex) { + return ((CreateIndex) statement).getIndex(); + } + if (statement instanceof CreateTable) { + CreateTable table = (CreateTable) statement; + return (IndexOptionSource) (table.getIndexes().isEmpty() + ? table.getTableConstraints().get(0) + : table.getIndexes().get(0)); + } + AlterExpression action = ((Alter) statement).getAlterExpressions().get(0); + return (IndexOptionSource) (action.getConstraint() != null ? action.getConstraint() + : action.getIndex()); + } + + private static KeyConstraint keyConstraint(Statement statement) { + return (KeyConstraint) definition(statement); + } + + private static Statement roundTrip(String sql, boolean mysql) throws Exception { + Consumer dialect = mysql ? parser -> parser.withDialect(Dialect.MYSQL) + : parser -> { + }; + Statement statement = CCJSqlParserUtil.parse(sql, dialect); + assertEquals(sql, statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(sql, deparsed.toString()); + assertEquals(sql, CCJSqlParserUtil.parse(sql, dialect).toString()); + return statement; + } + + private static void assertRendered(Statement statement, String expected) throws Exception { + assertEquals(expected, statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(expected, deparsed.toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected, + parser -> parser.withDialect(Dialect.MYSQL)).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlInlineCheckTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlInlineCheckTest.java new file mode 100644 index 0000000000..f01a2b08f2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlInlineCheckTest.java @@ -0,0 +1,112 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlInlineCheckTest { + @ParameterizedTest + @ValueSource(strings = {"", " ENFORCED", " NOT ENFORCED"}) + void sharesChecksAcrossCreateAndColumnAlterations(String enforcement) + throws JSQLParserException { + for (Dialect dialect : new Dialect[] {null, Dialect.MYSQL}) { + for (String prefix : new String[] {"CREATE TABLE t (", "ALTER TABLE t ADD COLUMN ", + "ALTER TABLE t MODIFY COLUMN ", "ALTER TABLE t CHANGE COLUMN old_a "}) { + String sql = prefix + "a INT CONSTRAINT ck CHECK (a > 7)" + enforcement; + if (prefix.startsWith("CREATE")) { + sql += ")"; + } + Statement statement = parse(sql, dialect); + ColumnDefinition column = statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList() + .get(0); + assertEquals(1, column.getColumnOptions().size()); + ColumnOption option = column.getColumnOptions().get(0); + assertEquals(ColumnOption.Kind.CONSTRAINT, option.getKind()); + CheckConstraint check = + assertInstanceOf(CheckConstraint.class, option.getConstraint()); + assertEquals("ck", check.getName()); + assertEquals(enforcement.isEmpty() ? null : !enforcement.contains("NOT"), + check.getEnforced()); + List seen = new ArrayList<>(); + ExpressionDeParser expression = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + seen.add(value.getValue()); + return getBuilder().append(value.getValue() + 100); + } + }; + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(expression, new SelectDeParser(), out)); + assertEquals(List.of(7L), seen, sql); + assertTrue(out.toString().contains("a > 107")); + assertTrue(statement.toString().contains("a > 7")); + check.setEnforced(false); + check.setExpression(CCJSqlParserUtil.parseExpression("a > 9")); + roundTrip(statement, dialect); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"CHECK (a > 0) NOT NULL", "CHECK (a > 0) CHECK (a < 100)", + "CHECK (a > 0) NOT ENFORCED COMMENT 'check; note'"}) + void preservesFollowingColumnOptions(String tail) throws JSQLParserException { + roundTrip(parse("CREATE TABLE t (a INT " + tail + ", b INT)", Dialect.MYSQL), + Dialect.MYSQL); + } + + @ParameterizedTest + @ValueSource(strings = {"", " NO INHERIT"}) + void preservesPostgreSql18InlineCheckEnforcement(String inheritance) + throws JSQLParserException { + CreateTable table = (CreateTable) parse( + "CREATE TABLE t (a INT CHECK (a > 0)" + inheritance + " NOT ENFORCED)", + Dialect.POSTGRESQL); + CheckConstraint check = assertInstanceOf(CheckConstraint.class, + table.getColumnDefinitions().get(0).getColumnOptions().get(0).getConstraint()); + assertEquals(false, check.getEnforced()); + assertEquals(!inheritance.isEmpty(), check.isNoInherit()); + roundTrip(table, Dialect.POSTGRESQL); + } + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> { + if (dialect != null) { + p.withDialect(dialect); + } + }); + } + + private static void roundTrip(Statement statement, Dialect dialect) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlRoutineCharacteristicsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlRoutineCharacteristicsTest.java new file mode 100644 index 0000000000..5593ec1835 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlRoutineCharacteristicsTest.java @@ -0,0 +1,168 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.CreateFunctionalStatement; +import net.sf.jsqlparser.statement.RoutineCharacteristic; +import net.sf.jsqlparser.statement.RoutineCharacteristic.Kind; +import net.sf.jsqlparser.statement.RoutineCharacteristic.SecurityClause; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlRoutineCharacteristicsTest { + + @ParameterizedTest + @CsvSource(delimiter = '|', quoteCharacter = '`', value = { + "CREATE PROCEDURE cp1() COMMENT 'proc comment' SELECT 1|COMMENT", + "CREATE FUNCTION cf1() RETURNS INT DETERMINISTIC COMMENT 'fn' RETURN 1" + + "|DETERMINISTIC COMMENT", + "CREATE PROCEDURE gp13() COMMENT 'c' LANGUAGE SQL SQL SECURITY INVOKER SELECT 1" + + "|COMMENT LANGUAGE_SQL SECURITY_INVOKER", + "CREATE FUNCTION gf14() RETURNS INT DETERMINISTIC RETURN 1|DETERMINISTIC", + "CREATE FUNCTION f1 (a INT) RETURNS INT DETERMINISTIC RETURN a + 1|DETERMINISTIC", + "CREATE DEFINER=CURRENT_USER FUNCTION f2 (s CHAR(20)) RETURNS CHAR(50) " + + "CHARSET utf8mb4 NO SQL RETURN CONCAT('Hello, ', s, '!')|NO_SQL", + "CREATE FUNCTION f() RETURNS VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin " + + "READS SQL DATA RETURN 'a'|READS_SQL_DATA", + "CREATE FUNCTION f() RETURNS SET('a', 'b') CHAR SET latin1 MODIFIES SQL DATA " + + "RETURN 'a'|MODIFIES_SQL_DATA", + "CREATE PROCEDURE p (IN a INT) NOT DETERMINISTIC CONTAINS SQL SQL SECURITY DEFINER " + + "BEGIN SELECT a; END|NOT_DETERMINISTIC CONTAINS_SQL SECURITY_DEFINER", + "CREATE FUNCTION f() RETURNS INT not deterministic comment 'x' return 1" + + "|NOT_DETERMINISTIC COMMENT", + "CREATE PROCEDURE p1 (IN a INT, OUT b INT) BEGIN SELECT a INTO b; END|``", + "CREATE DEFINER=CURRENT_USER PROCEDURE p3 () SELECT 1|``", + "CREATE FUNCTION f() RETURNS INT RETURN (SELECT deterministic FROM t)|``", + "CREATE PROCEDURE p() READS SQL DATA BEGIN DECLARE deterministic INT; END" + + "|READS_SQL_DATA"}) + void modelsCharacteristicsWithoutChangingTokens(String sql, String kinds) + throws JSQLParserException { + // Without a dialect, the declaration is only captured as tokens. + CreateFunctionalStatement plain = + (CreateFunctionalStatement) CCJSqlParserUtil.parse(sql); + assertTrue(plain.getCharacteristics().isEmpty()); + + for (Dialect dialect : new Dialect[] {Dialect.MYSQL, Dialect.MARIADB}) { + Consumer consumer = parser -> parser.withDialect(dialect); + CreateFunctionalStatement statement = + (CreateFunctionalStatement) CCJSqlParserUtil.parse(sql, consumer); + assertEquals(kinds.isEmpty() ? List.of() : List.of(kinds.split(" ")), + statement.getCharacteristics().stream() + .map(characteristic -> characteristic.getKind().name()) + .collect(Collectors.toList())); + + assertEquals(plain.getFunctionDeclarationParts(), + statement.getFunctionDeclarationParts()); + assertEquals(plain.toString(), statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(plain.toString(), deparsed.toString()); + assertEquals(statement.toString(), + CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString()); + + Statements script = CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer); + assertEquals(2, script.size()); + assertEquals(statement.getCharacteristics().size(), + ((CreateFunctionalStatement) script.get(0)).getCharacteristics().size()); + } + } + + @Test + void exposesCharacteristicValues() throws JSQLParserException { + CreateFunctionalStatement procedure = parse("CREATE PROCEDURE gp13() COMMENT 'c' " + + "LANGUAGE SQL SQL SECURITY INVOKER SELECT 1"); + List characteristics = procedure.getCharacteristics(); + assertEquals("c", characteristics.get(0).getComment().getNotExcapedValue()); + assertEquals("'c'", characteristics.get(0).getValue()); + assertEquals(SecurityClause.SQL_SECURITY, characteristics.get(2).getSecurityClause()); + assertEquals("SQL SECURITY INVOKER", characteristics.get(2).toString()); + assertThrows(UnsupportedOperationException.class, + () -> characteristics.add(new RoutineCharacteristic(Kind.NO_SQL))); + + CreateFunctionalStatement function = + parse("CREATE FUNCTION f() RETURNS INT not deterministic comment 'x' return 1"); + // The tokens keep the spelling; the characteristics render canonical keywords. + assertEquals("CREATE FUNCTION f ( ) RETURNS INT not deterministic comment 'x' return 1", + function.toString()); + assertEquals("NOT DETERMINISTIC", function.getCharacteristics().get(0).toString()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', quoteCharacter = '`', value = {"'it\\'s'|it's", "'it''s'|it's", + "\"dq\"|dq", "'a\\\\b'|a\\b", "'한글 설명 😀'|한글 설명 😀"}) + void decodesComments(String literal, String stored) throws JSQLParserException { + CreateFunctionalStatement statement = + parse("CREATE PROCEDURE p() COMMENT " + literal + " SELECT 1"); + RoutineCharacteristic comment = statement.getCharacteristics().get(0); + assertEquals(Kind.COMMENT, comment.getKind()); + assertEquals(literal, comment.getValue()); + assertEquals(stored, comment.getComment().getNotExcapedValue()); + assertEquals("CREATE PROCEDURE p ( ) COMMENT " + literal + " SELECT 1", + statement.toString()); + } + + @Test + void replacingTheTokensClearsTheCharacteristics() throws JSQLParserException { + CreateFunctionalStatement statement = + parse("CREATE FUNCTION gf14() RETURNS INT DETERMINISTIC RETURN 1"); + assertEquals(1, statement.getCharacteristics().size()); + + statement.addFunctionDeclarationParts("+", "1"); + assertEquals(1, statement.getCharacteristics().size()); + assertEquals("CREATE FUNCTION gf14 ( ) RETURNS INT DETERMINISTIC RETURN 1 + 1", + statement.toString()); + + statement.setFunctionDeclarationParts(List.of("g", "(", ")", "SELECT", "1")); + assertTrue(statement.getCharacteristics().isEmpty()); + assertEquals("CREATE FUNCTION g ( ) SELECT 1", statement.toString()); + + statement.setFunctionDeclarationParts(List.of("g", "(", ")", "NO", "SQL", "SELECT", "1"), + List.of(new RoutineCharacteristic(Kind.NO_SQL))); + assertEquals(Kind.NO_SQL, statement.getCharacteristics().get(0).getKind()); + assertEquals("CREATE FUNCTION g ( ) NO SQL SELECT 1", statement.toString()); + } + + @Test + void otherDialectsKeepOnlyTokens() throws JSQLParserException { + String postgres = "CREATE FUNCTION add(a integer, b integer) RETURNS integer " + + "LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT a + b $$"; + assertTrue(((CreateFunctionalStatement) CCJSqlParserUtil.parse(postgres, + parser -> parser.withDialect(Dialect.POSTGRESQL))).getCharacteristics().isEmpty()); + assertTrue(((CreateFunctionalStatement) CCJSqlParserUtil.parse(postgres)) + .getCharacteristics().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER FUNCTION f1 DETERMINISTIC", + "ALTER PROCEDURE p1 NOT DETERMINISTIC"}) + void alterKeepsRejectingDeterministic(String sql) throws JSQLParserException { + assertInstanceOf(UnsupportedStatement.class, + CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } + + private static CreateFunctionalStatement parse(String sql) throws JSQLParserException { + return (CreateFunctionalStatement) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.MYSQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlSharedTableOptionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlSharedTableOptionTest.java new file mode 100644 index 0000000000..c8e456aadc --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlSharedTableOptionTest.java @@ -0,0 +1,112 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpressionTableOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlSharedTableOptionTest { + @ParameterizedTest + @CsvSource(value = {"SECONDARY_ENGINE|NULL|SECONDARY_ENGINE", + "SECONDARY_ENGINE|'engine'|SECONDARY_ENGINE", + "SECONDARY_ENGINE|engine_name|SECONDARY_ENGINE", "AUTOEXTEND_SIZE|4M|AUTOEXTEND_SIZE", + "AUTOEXTEND_SIZE|0|AUTOEXTEND_SIZE", "INSERT_METHOD|NO|INSERT_METHOD", + "INSERT_METHOD|FIRST|INSERT_METHOD", "INSERT_METHOD|LAST|INSERT_METHOD", + "PACK_KEYS|DEFAULT|PACK_KEYS", "PACK_KEYS|1|PACK_KEYS", "PACK_KEYS|0|PACK_KEYS", + "DELAY_KEY_WRITE|2|DELAY_KEY_WRITE", "CHECKSUM|0|CHECKSUM", + "CONNECTION|'mysql://server/db/t'|CONNECTION", "COMPRESSION|'zlib'|COMPRESSION", + "ENCRYPTION|'N'|ENCRYPTION", "PASSWORD|'ignored'|PASSWORD", + "DATA DIRECTORY|'/tmp/data dir'|DATA_DIRECTORY", + "INDEX DIRECTORY|'/tmp/index dir'|INDEX_DIRECTORY", + "AUTO_INCREMENT|18446744073709551614|AUTO_INCREMENT"}, delimiter = '|', + quoteCharacter = '"') + void sharesCreateAndAlterModels(String name, String value, String kind) + throws JSQLParserException { + for (String equals : new String[] {" ", " = "}) { + String optionSql = name + equals + value; + CreateTable create = + (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t (id BIGINT) " + optionSql); + Alter alter = (Alter) CCJSqlParserUtil + .parse("ALTER TABLE t " + optionSql + ", ADD COLUMN extra INT"); + AlterExpressionTableOption action = assertInstanceOf(AlterExpressionTableOption.class, + alter.getAlterExpressions().get(0)); + TableOption option = action.getStructuredTableOption(); + assertEquals(TableOption.Kind.valueOf(kind), option.getKind()); + assertEquals(create.getTableOptions().get(0).toString(), option.toString()); + assertEquals(value, option.getValue()); + assertEquals(equals.contains("="), action.getUseEqual()); + assertEquals(optionSql, action.getTableOption()); + assertEquals(2, alter.getAlterExpressions().size()); + roundTrip(create); + roundTrip(alter); + option.setUseEquals(!option.isUseEquals()); + assertEquals(option.isUseEquals(), action.getUseEqual()); + roundTrip(alter); + action.setUseEqual(true); + assertTrue(option.isUseEquals()); + action.setTableOption("ENCRYPTION 'N'"); + assertNull(action.getStructuredTableOption()); + assertEquals("ENCRYPTION 'N'", action.toString()); + } + } + + @Test + void commasAndQuerySourceStayOutsideOptions() throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT) ENGINE=InnoDB, ENCRYPTION='N', PASSWORD='a,b' AS SELECT 1 AS id"); + assertEquals(3, table.getTableOptions().size()); + assertNotNull(table.getSelect()); + table.getTableOptions().get(1).setValue("'Y'"); + assertTrue(table.getTableOptionsStrings().contains("'Y'")); + roundTrip(table); + table.setTableOptionsStrings(List.of("ENCRYPTION", "=", "'N'")); + assertNull(table.getTableOptions()); + roundTrip(table); + } + + @ParameterizedTest + @ValueSource(strings = {"ENCRYPTION=", "PASSWORD=1", "DATA DIRECTORY='/tmp' INDEX DIRECTORY=", + "PACK_KEYS=2", "AUTOEXTEND_SIZE=-1", "INSERT_METHOD=UNKNOWN", "COMPRESSION=1", + "SECONDARY_ENGINE=", "AUTO_INCREMENT=-1", "AUTO_INCREMENT='5'", "ENCRYPTION='N',"}) + void rejectsMissingOrWrongCreateValues(String option) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("CREATE TABLE t (id INT) " + option)); + } + + @ParameterizedTest + @ValueSource(strings = {"VECTOR", "VECTOR(0)", "VECTOR(1)", "VECTOR(2048)", "VECTOR(16383)"}) + void preservesMySql9VectorColumnsBeforeTableOptions(String type) throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (v " + type + ") ENCRYPTION='N'"); + assertEquals(type, table.getColumnDefinitions().get(0).getColDataType().toString() + .replace(" ", "")); + assertEquals(TableOption.Kind.ENCRYPTION, table.getTableOptions().get(0).getKind()); + roundTrip(table); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), CCJSqlParserUtil.parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java new file mode 100644 index 0000000000..4975f0700b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java @@ -0,0 +1,221 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyReference; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintKind; +import net.sf.jsqlparser.statement.create.table.TableOption; +import org.junit.jupiter.api.Test; + +/** Regression tests for MySQL table definitions and their structured AST representation. */ +public class MySqlTableDefinitionTest { + + @Test + public void testConstraintBeforeColumnDefinition() throws JSQLParserException { + CreateTable table = + parseMySql("CREATE TABLE inventory (PRIMARY KEY (id), id INT NOT NULL)"); + + assertEquals(2, table.getTableElements().size()); + assertInstanceOf(KeyConstraint.class, table.getTableElements().get(0)); + assertInstanceOf(ColumnDefinition.class, table.getTableElements().get(1)); + assertEquals("PRIMARY KEY", table.getTableConstraints().get(0).getType()); + assertReparse(table); + } + + @Test + public void testNamedPrimaryAndSpecializedIndexes() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE documents (id INT, body TEXT, data JSON, " + + "PRIMARY KEY pk_documents (id), FULLTEXT INDEX ft_body (body), " + + "INDEX ((CAST(data AS CHAR(30)))))"); + + KeyConstraint primary = (KeyConstraint) table.getTableConstraints().get(0); + assertEquals("pk_documents", primary.getIndexName()); + assertEquals(ConstraintKind.PRIMARY_KEY, primary.getKind()); + assertEquals(Index.Kind.FULLTEXT, table.getIndexes().get(0).getKind()); + assertTrue(table.getIndexes().get(1).getColumns().get(0).isExpression()); + assertReparse(table); + } + + @Test + public void testFulltextAndSpatialIndexes() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE map_entry (body TEXT, coordinates POINT, " + + "FULLTEXT KEY ft_body (body), SPATIAL INDEX sp_coordinates (coordinates))"); + + assertEquals(Index.Kind.FULLTEXT, table.getIndexes().get(0).getKind()); + assertEquals("KEY", table.getIndexes().get(0).getType().split(" ")[1]); + assertEquals(Index.Kind.SPATIAL, table.getIndexes().get(1).getKind()); + assertReparse(table); + } + + @Test + public void testStructuredColumnOptions() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE child_record (id SMALLINT UNSIGNED " + + "SERIAL DEFAULT VALUE NOT NULL, parent_id INT REFERENCES parent_record(id) " + + "MATCH FULL ON UPDATE CASCADE ON DELETE SET NULL)"); + + ColumnDefinition id = table.getColumnDefinitions().get(0); + assertTrue(id.isSerialDefaultValue()); + + ForeignKeyReference reference = table.getColumnDefinitions().get(1) + .getForeignKeyReference(); + assertNotNull(reference); + assertEquals("parent_record", reference.getTable().getName()); + assertEquals(Arrays.asList("id"), reference.getReferencedColumnNames()); + assertEquals(ForeignKeyReference.MatchType.FULL, reference.getMatchType()); + assertNotNull(reference.getReferentialAction( + net.sf.jsqlparser.statement.ReferentialAction.Type.UPDATE)); + assertReparse(table); + } + + @Test + public void testNationalCharacterTypesAndOrderedModifiers() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE type_samples (label NATIONAL CHARACTER " + + "VARYING(64), code NATIONAL CHAR(8), flags BIGINT ZEROFILL SIGNED UNSIGNED " + + "SIGNED ZEROFILL)"); + + assertEquals(ColDataType.NationalCharacterType.VARCHAR, + table.getColumnDefinitions().get(0).getColDataType().getNationalCharacterType()); + assertEquals(ColDataType.NationalCharacterType.CHAR, + table.getColumnDefinitions().get(1).getColDataType().getNationalCharacterType()); + assertEquals(Arrays.asList(ColDataType.TypeModifier.ZEROFILL, + ColDataType.TypeModifier.SIGNED, ColDataType.TypeModifier.UNSIGNED, + ColDataType.TypeModifier.SIGNED, ColDataType.TypeModifier.ZEROFILL), + table.getColumnDefinitions().get(2).getColDataType().getTypeModifiers()); + assertReparse(table); + } + + @Test + public void testNationalCharacterAliases() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE localized_text (short_name NCHAR(16), " + + "long_name NVARCHAR(255), legacy_name NCHAR VARCHAR(64))"); + + assertEquals(ColDataType.NationalCharacterType.CHAR, + table.getColumnDefinitions().get(0).getColDataType().getNationalCharacterType()); + assertEquals(ColDataType.NationalCharacterType.VARCHAR, + table.getColumnDefinitions().get(1).getColDataType().getNationalCharacterType()); + assertEquals(ColDataType.NationalCharacterType.VARCHAR, + table.getColumnDefinitions().get(2).getColDataType().getNationalCharacterType()); + assertReparse(table); + } + + @Test + public void testStructuredMySqlTableOptionsAndKeywordColumn() throws JSQLParserException { + CreateTable table = + parseMySql("CREATE TABLE log_entry (Position BIGINT, File VARCHAR(255)) " + + "ENGINE=CSV DEFAULT CHAR SET=utf8mb4"); + + assertEquals("File", table.getColumnDefinitions().get(1).getColumnName()); + assertEquals(TableOption.Kind.ENGINE, table.getTableOptions().get(0).getKind()); + assertEquals("CSV", table.getTableOptions().get(0).getValue()); + assertEquals(TableOption.Kind.CHARACTER_SET, table.getTableOptions().get(1).getKind()); + assertEquals("utf8mb4", table.getTableOptions().get(1).getValue()); + assertReparse(table); + + CreateTable quotedEngine = parseMySql("CREATE TABLE archive_entry (id INT) " + + "ENGINE='InnoDB'"); + assertEquals("'InnoDB'", quotedEngine.getTableOption(TableOption.Kind.ENGINE) + .orElseThrow().getValue()); + assertReparse(quotedEngine); + + CreateTable commonOptions = parseMySql("CREATE TABLE table_options " + + "(id BIGINT AUTO_INCREMENT PRIMARY KEY) ENGINE=InnoDB " + + "DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=7 " + + "COMMENT='archive'"); + assertEquals("utf8mb4_bin", commonOptions.getTableOption(TableOption.Kind.COLLATE) + .orElseThrow().getValue()); + assertEquals("7", commonOptions.getTableOption(TableOption.Kind.AUTO_INCREMENT) + .orElseThrow().getValue()); + assertEquals("'archive'", commonOptions.getTableOption(TableOption.Kind.COMMENT) + .orElseThrow().getValue()); + assertReparse(commonOptions); + } + + @Test + public void testEscapedBacktickIdentifier() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE `odd``name` (`value``part` INT)"); + + assertEquals("`odd``name`", table.getTable().getName()); + assertEquals("`value``part`", table.getColumnDefinitions().get(0).getColumnName()); + assertReparse(table); + } + + @Test + public void testTableForeignKeyUsesStructuredReference() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE order_line (customer_id BIGINT, " + + "CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customer(id) " + + "MATCH FULL ON DELETE CASCADE)"); + + ForeignKeyConstraint foreignKey = (ForeignKeyConstraint) table.getTableConstraints().get(0); + assertEquals(ConstraintKind.FOREIGN_KEY, foreignKey.getKind()); + assertEquals("customer", foreignKey.getReference().getTable().getName()); + assertEquals(ForeignKeyReference.MatchType.FULL, foreignKey.getMatchType()); + assertReparse(table); + } + + @Test + public void testAlterColumnUsesStructuredOptions() throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse("ALTER TABLE child_record ADD COLUMN " + + "parent_id INT REFERENCES parent_record(id) MATCH FULL ON DELETE CASCADE", + parser -> parser.withDialect(Dialect.MYSQL)); + + AlterExpression.ColumnDataType column = + alter.getAlterExpressions().get(0).getColDataTypeList().get(0); + assertEquals("parent_record", column.getForeignKeyReference().getTable().getName()); + assertEquals(ForeignKeyReference.MatchType.FULL, + column.getForeignKeyReference().getMatchType()); + CCJSqlParserUtil.parse(alter.toString(), parser -> parser.withDialect(Dialect.MYSQL)); + } + + @Test + public void testCreateAndAlterTableIndexParity() throws JSQLParserException { + CreateTable create = parseMySql("CREATE TABLE search_item (id INT, body TEXT, " + + "PRIMARY KEY pk_search (id), FULLTEXT INDEX ft_body (body))"); + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE search_item ADD PRIMARY KEY pk_search (id), " + + "ADD FULLTEXT INDEX ft_body (body)", + parser -> parser.withDialect(Dialect.MYSQL)); + + assertEquals(create.getTableConstraints().get(0).getClass(), + alter.getAlterExpressions().get(0).getConstraint().getClass()); + assertEquals(create.getTableConstraints().get(0).getKind(), + alter.getAlterExpressions().get(0).getConstraint().getKind()); + assertEquals(create.getTableConstraints().get(0).toString(), + alter.getAlterExpressions().get(0).getConstraint().toString()); + assertEquals(create.getIndexes().get(0).toString(), + alter.getAlterExpressions().get(1).getIndex().toString()); + CCJSqlParserUtil.parse(alter.toString(), parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static CreateTable parseMySql(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static void assertReparse(CreateTable table) throws JSQLParserException { + parseMySql(table.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableOptionsTest.java new file mode 100644 index 0000000000..bbfab5388b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableOptionsTest.java @@ -0,0 +1,96 @@ +/*- + * #%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.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlTableOptionsTest { + @ParameterizedTest + @ValueSource(strings = { + "STATS_PERSISTENT=1, STATS_AUTO_RECALC=1", + "STATS_PERSISTENT DEFAULT, STATS_AUTO_RECALC=0 STATS_SAMPLE_PAGES=32", + "ENGINE=InnoDB, DEFAULT CHARSET=utf8mb4", + "ENGINE=InnoDB, COMMENT='commas, in text', AUTO_INCREMENT=10", + "UNION=(t1,t2) ENGINE=MRG_MyISAM", + "ENGINE=MRG_MyISAM, UNION (db.t1, `db`.`t2`)", + "UNION=() ENGINE=MRG_MyISAM", + "ENGINE=InnoDB, STATS_PERSISTENT=1 PARTITION BY HASH(id) PARTITIONS 2", + "ENGINE=InnoDB, STATS_AUTO_RECALC=DEFAULT AS SELECT 1 AS other" + }) + void parsesOptionSeparatorsAndValues(String options) throws JSQLParserException { + CreateTable table = parse("CREATE TABLE t (id INT) " + options); + assertRoundTrip(table); + assertEquals(table.toString(), CCJSqlParserUtil.parse(table.toString()).toString()); + } + + @Test + void statisticsOptionsHaveIndependentValues() throws JSQLParserException { + CreateTable table = parse("CREATE TABLE t (id INT) " + + "STATS_PERSISTENT=1, STATS_AUTO_RECALC=DEFAULT"); + TableOption persistent = + table.getTableOption(TableOption.Kind.STATS_PERSISTENT).orElseThrow(); + assertEquals("1", persistent.getValue()); + persistent.setValue("0"); + assertThat(table.toString()).contains("STATS_PERSISTENT = 0 STATS_AUTO_RECALC = DEFAULT"); + assertRoundTrip(table); + } + + @Test + void unionSourcesAreTraversableAndMutable() throws JSQLParserException { + CreateTable table = + parse("CREATE TABLE merged (id INT) UNION=(db.t1,t2) ENGINE=MRG_MyISAM"); + TableOption union = table.getTableOption(TableOption.Kind.UNION).orElseThrow(); + assertEquals(2, union.getUnionTables().size()); + assertEquals("db", union.getUnionTables().get(0).getSchemaName()); + union.getUnionTables().get(0).setName("changed"); + assertEquals("(db.changed, t2)", union.getValue()); + assertThat(table.getTableOptionsStrings()).contains("(db.changed, t2)"); + assertThat(new TablesNamesFinder().getTables(table)) + .containsExactlyInAnyOrder("merged", "db.changed", "t2"); + assertRoundTrip(table); + union.getUnionTables().clear(); + assertEquals("()", union.getValue()); + assertRoundTrip(table); + union.setValue("(replacement)"); + assertEquals("(replacement)", union.getValue()); + assertRoundTrip(table); + } + + @ParameterizedTest + @ValueSource(strings = {", ENGINE=InnoDB", "ENGINE=InnoDB,", "ENGINE=InnoDB,, COMMENT='x'", + "UNION=(t1,)", "UNION=", "STATS_PERSISTENT=2", "STATS_AUTO_RECALC=-1"}) + void rejectsIncompleteListsAndInvalidBooleanOptions(String options) { + assertThrows(JSQLParserException.class, () -> parse("CREATE TABLE t (id INT) " + options)); + } + + private static CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static void assertRoundTrip(CreateTable table) throws JSQLParserException { + StringBuilder buffer = new StringBuilder(); + table.accept(new StatementDeParser(buffer), null); + assertEquals(table.toString(), buffer.toString()); + assertEquals(table.toString(), parse(buffer.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlViewOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlViewOptionsTest.java new file mode 100644 index 0000000000..29eeb07b89 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlViewOptionsTest.java @@ -0,0 +1,108 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.view.*; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlViewOptionsTest { + @ParameterizedTest + @ValueSource(strings = {"ALGORITHM = MERGE", "ALGORITHM = TEMPTABLE", + "ALGORITHM = UNDEFINED", "DEFINER = CURRENT_USER", "DEFINER = CURRENT_USER()", + "DEFINER = 'user'@'localhost'", "DEFINER = `user`@`localhost`", + "SQL SECURITY INVOKER", "SQL SECURITY DEFINER", + "ALGORITHM = MERGE DEFINER = CURRENT_USER SQL SECURITY INVOKER"}) + void createAndAlterSharePrefix(String options) throws JSQLParserException { + for (String operation : new String[] {"CREATE", "CREATE OR REPLACE", "ALTER"}) { + String sql = operation + " " + options + " VIEW v AS SELECT id FROM t"; + Statement statement = parse(sql); + assertEquals(sql, statement.toString()); + assertRoundTrip(statement); + assertTrue(new TablesNamesFinder().getTables(statement).contains("t")); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.MYSQL)).size()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CHECK OPTION", "LOCAL CHECK OPTION", "CASCADED CHECK OPTION"}) + void checkOptionTailIsShared(String tail) throws JSQLParserException { + for (String operation : new String[] {"CREATE", "ALTER"}) { + String sql = + operation + " SQL SECURITY INVOKER VIEW v AS SELECT id FROM t WITH " + tail; + Statement statement = parse(sql); + assertEquals(sql, statement.toString()); + assertRoundTrip(statement); + } + } + + @Test + void optionsAndDefinerAreMutable() throws JSQLParserException { + CreateView view = (CreateView) parse( + "CREATE ALGORITHM=MERGE DEFINER=CURRENT_USER() VIEW v AS SELECT 1"); + MySqlViewOptions options = view.getMySqlOptions(); + assertEquals(MySqlViewOptions.Algorithm.MERGE, options.getAlgorithm()); + assertTrue(options.getDefiner().isCurrentUserParentheses()); + options.getDefiner().setUser(new StringValue("owner")); + options.getDefiner().setHost(new StringValue("localhost")); + options.setAlgorithm(MySqlViewOptions.Algorithm.TEMPTABLE); + options.setSecurity(MySqlViewOptions.Security.INVOKER); + assertEquals( + "CREATE ALGORITHM = TEMPTABLE DEFINER = 'owner'@'localhost' SQL SECURITY INVOKER VIEW v AS SELECT 1", + view.toString()); + assertRoundTrip(view); + view.setMySqlOptions(null); + assertEquals("CREATE VIEW v AS SELECT 1", view.toString()); + } + + @Test + void sharedAccountParserPreservesTriggerDispatch() throws JSQLParserException { + for (String account : new String[] {"CURRENT_USER", "CURRENT_USER()", "'u'@'localhost'"}) { + CreateTrigger trigger = (CreateTrigger) parse("CREATE DEFINER = " + account + + " TRIGGER tr BEFORE INSERT ON t FOR EACH ROW SET @a = 1"); + assertEquals(account, trigger.getDefiner().toString()); + assertRoundTrip(trigger); + } + } + + @Test + void invalidPrefixValuesFail() { + for (String sql : new String[] {"CREATE ALGORITHM=INVALID VIEW v AS SELECT 1", + "ALTER SQL SECURITY PUBLIC VIEW v AS SELECT 1"}) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + if (statement instanceof net.sf.jsqlparser.statement.UnsupportedStatement) { + throw new JSQLParserException("Expected a structured statement"); + } + return statement; + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PartitionKeyMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PartitionKeyMutationTest.java new file mode 100644 index 0000000000..3e2689ba8e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PartitionKeyMutationTest.java @@ -0,0 +1,89 @@ +/*- + * #%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.statement.create; + +import net.sf.jsqlparser.statement.create.table.KeyElement; +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; + +class PartitionKeyMutationTest { + @Test + void replacingSingleKeyWithSeveralKeysChangesBothRenderers() throws JSQLParserException { + CreateTable table = parse("CREATE TABLE t(id INT,other INT) PARTITION BY RANGE(id)", + Dialect.POSTGRESQL); + table.getPartitioning().setExpressionList( + new ExpressionList(new Column("other"), new Column("id"))); + assertRoundTrip(table, + "CREATE TABLE t (id INT, other INT) PARTITION BY RANGE (other, id)", + Dialect.POSTGRESQL); + assertNull(table.getPartitioning().getExpression()); + } + + @Test + void switchingHashToKeyUsesNewColumnNames() throws JSQLParserException { + CreateTable table = parse("CREATE TABLE t(id INT,other INT) PARTITION BY HASH(id)", + Dialect.MYSQL); + TablePartitioning partition = table.getPartitioning(); + partition.setType(TablePartitioning.Type.KEY); + partition.setColumns(new ExpressionList<>(new Column("other"))); + assertRoundTrip(table, "CREATE TABLE t (id INT, other INT) PARTITION BY KEY (other)", + Dialect.MYSQL); + partition.setType(TablePartitioning.Type.HASH); + partition.setExpression(new Column("id")); + assertNull(partition.getColumns()); + assertRoundTrip(table, "CREATE TABLE t (id INT, other INT) PARTITION BY HASH (id)", + Dialect.MYSQL); + } + + @Test + void attributedKeysReplaceAndAreReplacedByPlainKeys() throws JSQLParserException { + CreateTable table = parse("CREATE TABLE t(id INT,other INT) PARTITION BY RANGE(id,other)", + Dialect.POSTGRESQL); + TablePartitioning partition = table.getPartitioning(); + partition.setKeyColumns( + List.of(new KeyElement("other").withOperatorClass("int4_ops"))); + assertNull(partition.getExpressionList()); + assertRoundTrip(table, + "CREATE TABLE t (id INT, other INT) PARTITION BY RANGE (other int4_ops)", + Dialect.POSTGRESQL); + partition.setExpression(new Column("id")); + assertNull(partition.getKeyColumns()); + assertRoundTrip(table, "CREATE TABLE t (id INT, other INT) PARTITION BY RANGE (id)", + Dialect.POSTGRESQL); + partition.setColumns(null); + partition.setExpressionList(null); + partition.setKeyColumns(null); + assertRoundTrip(table, "CREATE TABLE t (id INT, other INT) PARTITION BY RANGE (id)", + Dialect.POSTGRESQL); + } + + private static CreateTable parse(String sql, Dialect dialect) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + } + + private static void assertRoundTrip(CreateTable table, String expected, Dialect dialect) + throws JSQLParserException { + assertEquals(expected, table.toString()); + StringBuilder output = new StringBuilder(); + table.accept(new StatementDeParser(output), null); + assertEquals(expected, output.toString()); + assertEquals(expected, parse(output.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java new file mode 100644 index 0000000000..065ec7f495 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java @@ -0,0 +1,128 @@ +/*- + * #%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.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PartitionTraversalTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE parent ATTACH PARTITION child FOR VALUES FROM (0) TO (10)", + "ALTER TABLE parent ATTACH PARTITION child DEFAULT", + "ALTER TABLE parent DETACH PARTITION child CONCURRENTLY", + "ALTER TABLE parent EXCHANGE PARTITION p0 WITH TABLE child WITHOUT VALIDATION" + }) + void finderIncludesPartitionTables(String sql) throws JSQLParserException { + assertThat(TablesNamesFinder.findTables(sql)).containsExactlyInAnyOrder("parent", "child"); + } + + @Test + void visitorReceivesPartitionKeysBoundsAndContext() throws JSQLParserException { + List visited = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + assertEquals("context", context); + visited.add(column.getColumnName()); + return null; + } + + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + visited.add(value.toString()); + return null; + } + }; + StatementVisitorAdapter visitor = + new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)); + CCJSqlParserUtil.parse("CREATE TABLE t (id INT) PARTITION BY RANGE (id) " + + "SUBPARTITION BY HASH (id + 1) SUBPARTITIONS 2") + .accept(visitor, "context"); + CCJSqlParserUtil + .parse("ALTER TABLE parent ATTACH PARTITION child FOR VALUES FROM (0) TO (10)") + .accept(visitor, "context"); + assertThat(visited).containsExactly("id", "id", "1", "0", "10"); + } + + @Test + void rangeMarkersAreNotVisitedAsColumnReferences() throws JSQLParserException { + List columns = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + columns.add(column.getColumnName()); + return null; + } + }; + CCJSqlParserUtil.parse("ALTER TABLE parent ATTACH PARTITION child " + + "FOR VALUES FROM (MINVALUE) TO (MAXVALUE)") + .accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + null); + assertThat(columns).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (id INT) PARTITION BY HASH (id + 1) PARTITIONS 2", + "ALTER TABLE t PARTITION BY RANGE COLUMNS (id)", + "CREATE TABLE child PARTITION OF parent FOR VALUES FROM (0) TO (10)", + "CREATE TABLE child PARTITION OF parent FOR VALUES FROM (MINVALUE) TO (MAXVALUE)", + "ALTER TABLE parent ATTACH PARTITION child FOR VALUES IN (1, 2)", + "ALTER TABLE parent ATTACH PARTITION child FOR VALUES WITH (MODULUS 4, REMAINDER 1)", + "ALTER TABLE parent ATTACH PARTITION child DEFAULT" + }) + void deparserUsesPartitionExpressionVisitor(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("mapped_").append(column.getColumnName()); + } + + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + String expected = statement.toString() + .replace("HASH (id + 1)", "HASH (mapped_id + 101)") + .replace("COLUMNS (id)", "COLUMNS (mapped_id)") + .replace("FROM (0) TO (10)", "FROM (100) TO (110)") + .replace("IN (1, 2)", "IN (101, 102)") + .replace("MODULUS 4, REMAINDER 1", "MODULUS 104, REMAINDER 101"); + assertEquals(expected, output.toString()); + StringBuilder plain = new StringBuilder(); + statement.accept(new StatementDeParser(plain), null); + assertEquals(statement.toString(), plain.toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java new file mode 100644 index 0000000000..23d8442f92 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java @@ -0,0 +1,58 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import org.junit.jupiter.api.Test; + +/** PostgreSQL {@code CREATE INDEX} syntax verified against PostgreSQL 18.6. */ +public class PostgreSQLCreateIndexTest { + + @Test + public void testCreateIndexConcurrently() throws JSQLParserException { + String sql = "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS pg_idx_email " + + "ON pg_index_test USING btree (email)"; + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + + assertTrue(createIndex.isConcurrently()); + assertTrue(createIndex.isUsingIfNotExists()); + assertFalse(createIndex.isOnly()); + assertEquals("pg_idx_email", createIndex.getIndex().getName()); + } + + @Test + public void testCreateIndexWithOmittedNameAndOnly() throws JSQLParserException { + String sql = "CREATE INDEX ON ONLY pg_index_test USING btree (id DESC)"; + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + + assertNull(createIndex.getIndex().getName()); + assertTrue(createIndex.isOnly()); + assertFalse(createIndex.isConcurrently()); + } + + @Test + public void testCreateNamedIndexOnOnlyTable() throws JSQLParserException { + String sql = "CREATE INDEX pg_idx_only ON ONLY pg_index_test (id)"; + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + + assertEquals("pg_idx_only", createIndex.getIndex().getName()); + assertTrue(createIndex.isOnly()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessDdlTest.java new file mode 100644 index 0000000000..c919603b1c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessDdlTest.java @@ -0,0 +1,290 @@ +/*- + * #%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.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.trigger.TransitionRelation; +import net.sf.jsqlparser.statement.create.trigger.TriggerEvent; +import net.sf.jsqlparser.statement.create.user.CreateUser; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; +import net.sf.jsqlparser.statement.grant.Grant; +import net.sf.jsqlparser.statement.grant.GrantOption; +import net.sf.jsqlparser.statement.grant.Privilege; +import net.sf.jsqlparser.statement.grant.PrivilegeTarget; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.RoleOption; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlAccessDdlTest { + static Stream statements() throws Exception { + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + PostgreSqlAccessDdlTest.class.getResourceAsStream("/postgresql/access-ddl.sql"), + StandardCharsets.UTF_8))) { + return reader.lines() + .filter(line -> !line.trim().isEmpty() && !line.trim().startsWith("--")) + .collect(Collectors.toList()).stream(); + } + } + + @ParameterizedTest + @MethodSource("statements") + void testRoundTripAndValidation(String sql) throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql); + assertThat(statement).isInstanceOfAny(CreateRole.class, AlterRole.class, Grant.class, + Revoke.class, AlterDefaultPrivileges.class, CreateTrigger.class); + assertThat(CCJSqlParserUtil.parse(statement.toString()).toString()) + .isEqualTo(statement.toString()); + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(statement.getFeatures().getUncertain()) + .doesNotContain(StmtFeature.MODIFIES_DATA); + validateNoErrors(sql, 1, DatabaseType.POSTGRESQL); + } + + @Test + void testRoleOptionsAndAliases() throws Exception { + CreateRole role = (CreateRole) CCJSqlParserUtil + .parse("CREATE ROLE app WITH REPLICATION LOGIN CONNECTION LIMIT -1 PASSWORD NULL"); + assertThat(role.getCommand()).isEqualTo(CreateRole.Command.ROLE); + assertThat(role.isUseWith()).isTrue(); + assertThat(role.getOptions()).extracting(RoleOption::getKind).containsExactly( + RoleOption.Kind.REPLICATION, RoleOption.Kind.LOGIN, + RoleOption.Kind.CONNECTION_LIMIT, RoleOption.Kind.PASSWORD); + assertThat(((LongValue) role.getOptions().get(2).getValue()).getValue()).isEqualTo(-1); + assertThat(((CreateRole) CCJSqlParserUtil.parse("CREATE ROLE app")).getOptions()).isEmpty(); + assertThat(CCJSqlParserUtil.parse("CREATE USER app")).isInstanceOf(CreateUser.class); + CreateRole user = (CreateRole) CCJSqlParserUtil.parse("CREATE USER app", + p -> p.withDialect(Dialect.POSTGRESQL)); + assertThat(user.getCommand()).isEqualTo(CreateRole.Command.USER); + assertThat(user.getOptions()).isEmpty(); + assertThat(CCJSqlParserUtil + .parse("CREATE USER 'app'@'localhost' IDENTIFIED BY 'example-only'")) + .isInstanceOf(CreateUser.class); + assertThat(TablesNamesFinder.findTables(role.toString())).isEmpty(); + } + + @Test + void testAlterRoleSettings() throws Exception { + AlterRole role = (AlterRole) CCJSqlParserUtil + .parse("ALTER ROLE ALL IN DATABASE appdb SET search_path TO app, public"); + assertThat(role.getAction()).isEqualTo(AlterRole.Action.SET); + assertThat(role.getName()).isEqualTo("ALL"); + assertThat(role.getDatabase()).isEqualTo("appdb"); + assertThat(role.getValues()).hasSize(2); + role.getValues().set(0, new Column("new_schema")); + assertThat(role.toString()).endsWith("TO new_schema, public"); + assertThat(TablesNamesFinder.findTables(role.toString())).isEmpty(); + } + + @Test + void testTypedPrivilegesAndLegacyMutation() throws Exception { + Grant grant = (Grant) CCJSqlParserUtil.parse( + "GRANT SELECT (id), UPDATE (name) ON TABLE app.users, app.orders TO app WITH GRANT OPTION"); + assertThat(grant.getPrivilegeItems().get(0).getKind()).isEqualTo(Privilege.Kind.SELECT); + assertThat(grant.getPrivilegeItems().get(0).getColumns().get(0).getColumnName()) + .isEqualTo("id"); + assertThat(grant.getTarget().getKind()).isEqualTo(PrivilegeTarget.Kind.TABLE); + assertThat(grant.getTarget().getNames()).hasSize(2); + assertThat(grant.getOption().getKind()).isEqualTo(GrantOption.Kind.GRANT); + grant.addPrivileges("INSERT"); + assertThat(grant.getPrivilegeItems().get(0).getColumns()).hasSize(1); + grant.getPrivileges().set(1, "DELETE"); + grant.getUsers().add("reader"); + grant.getObjectNameParts().set(1, "new_users"); + assertThat(grant.getPrivilegeItems().get(1).getKind()).isEqualTo(Privilege.Kind.DELETE); + assertThat(grant.toString()).contains("app.new_users, app.orders TO app, reader"); + assertThat(TablesNamesFinder.findTables(grant.toString())) + .containsExactlyInAnyOrder("app.new_users", "app.orders"); + Grant created = new Grant().addPrivileges("SELECT") + .withObjectName(Arrays.asList("app", "users")).addUsers("app"); + assertThat(created.toString()).isEqualTo("GRANT SELECT ON app.users TO app"); + assertThat(created.getTarget().getNames()).containsExactly(Arrays.asList("app", "users")); + created.setObjectName(Arrays.asList("catalog", null, "users")); + assertThat(created.toString()).isEqualTo("GRANT SELECT ON catalog..users TO app"); + } + + @Test + void testRoutineSignaturesAndNonRelationTargets() throws Exception { + Grant grant = (Grant) CCJSqlParserUtil + .parse("GRANT EXECUTE ON FUNCTION app.f(IN arg integer), app.g(), app.h TO app"); + List routines = grant.getTarget().getRoutines(); + assertThat(routines).hasSize(3); + assertThat(routines.get(0).getArguments().get(0).getMode()) + .isEqualTo(RoutineReference.Argument.Mode.IN); + assertThat(routines.get(0).getArguments().get(0).getName()).isEqualTo("arg"); + assertThat(routines.get(1).getArguments()).isEmpty(); + assertThat(routines.get(2).getArguments()).isNull(); + assertThat(TablesNamesFinder.findTables(grant.toString())).isEmpty(); + assertThat( + TablesNamesFinder.findTables("GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader")) + .isEmpty(); + assertThat(TablesNamesFinder.findTables("GRANT USAGE ON SCHEMA app TO reader")).isEmpty(); + } + + @Test + void testRevokeAndDefaults() throws Exception { + Revoke revoke = (Revoke) CCJSqlParserUtil + .parse("REVOKE ADMIN OPTION FOR reader, writer FROM app GRANTED BY owner CASCADE"); + assertThat(revoke.getOptionFor()).isEqualTo(GrantOption.Kind.ADMIN); + assertThat(revoke.getClause().getRoles()).containsExactly("reader", "writer"); + assertThat(revoke.getClause().getGrantedBy()).isEqualTo("owner"); + assertThat(revoke.getBehavior()).isEqualTo(CascadeBehavior.CASCADE); + AlterDefaultPrivileges defaults = (AlterDefaultPrivileges) CCJSqlParserUtil.parse( + "ALTER DEFAULT PRIVILEGES FOR USER owner IN SCHEMA app GRANT SELECT ON TABLES TO reader"); + assertThat(defaults.getRoleKeyword()).isEqualTo(AlterDefaultPrivileges.RoleKeyword.USER); + assertThat(defaults.getGrant().getTarget().isDefaultPrivileges()).isTrue(); + assertThat(defaults.getSchemas()).containsExactly("app"); + assertThat(TablesNamesFinder.findTables(defaults.toString())).isEmpty(); + } + + @Test + void testTriggerFields() throws Exception { + CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse( + "CREATE CONSTRAINT TRIGGER check_parent AFTER INSERT OR UPDATE ON child FROM parent DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN (NEW.id > 0) EXECUTE PROCEDURE app.check_parent('id')"); + assertThat(trigger.getEvent()).isEqualTo(CreateTrigger.Event.INSERT); + assertThat(trigger.getEvents()).extracting(TriggerEvent::getEvent) + .containsExactly(CreateTrigger.Event.INSERT, CreateTrigger.Event.UPDATE); + assertThat(trigger.getConstraintAttributes().getInitially()) + .isEqualTo(ConstraintAttributes.Initially.DEFERRED); + assertThat(trigger.getBody()).isNull(); + assertThat(trigger.getRoutine().getName()).isEqualTo("app.check_parent"); + assertThat(trigger.getExecuteKeyword()).isEqualTo(CreateTrigger.ExecuteKeyword.PROCEDURE); + assertThat(TablesNamesFinder.findTables(trigger.toString())) + .containsExactlyInAnyOrder("child", "parent"); + CreateTrigger transition = (CreateTrigger) CCJSqlParserUtil.parse( + "CREATE TRIGGER audit AFTER INSERT ON users REFERENCING NEW TABLE AS inserted EXECUTE FUNCTION audit_all()"); + assertThat(transition.getOrientation()).isNull(); + assertThat(transition.getTransitionRelations().get(0).getImage()) + .isEqualTo(TransitionRelation.Image.NEW); + assertThat(transition.getTransitionRelations().get(0).isUseAs()).isTrue(); + assertThat(TablesNamesFinder.findTables(transition.toString())).containsExactly("users"); + } + + @Test + void testVisitorsAndDeparser() throws Exception { + Statement trigger = CCJSqlParserUtil.parse( + "CREATE TRIGGER audit BEFORE UPDATE OF id ON users FOR EACH ROW WHEN (NEW.id > 0) EXECUTE FUNCTION audit_row('example')"); + List columns = new ArrayList<>(); + List strings = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + assertThat(context).isEqualTo("access"); + columns.add(column.getColumnName()); + return null; + } + + @Override + public Void visit(StringValue value, S context) { + assertThat(context).isEqualTo("access"); + strings.add(value.getValue()); + return null; + } + }; + StatementVisitorAdapter visitor = + new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)); + trigger.accept(visitor, "access"); + CCJSqlParserUtil.parse("GRANT SELECT (name) ON users TO app").accept(visitor, "access"); + CCJSqlParserUtil.parse("CREATE ROLE app PASSWORD 'example-only'").accept(visitor, "access"); + assertThat(columns).containsExactly("id", "id", "name"); + assertThat(strings).containsExactly("example", "example-only"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser deparser = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + return getBuilder().append("'changed'"); + } + }; + trigger.accept(new StatementDeParser(deparser, new SelectDeParser(), output)); + assertThat(output.toString()).endsWith("audit_row('changed')"); + assertThat(trigger.toString()).endsWith("audit_row('example')"); + } + + @Test + void testProgrammaticTriggerAndRole() throws Exception { + CreateTrigger trigger = new CreateTrigger(); + trigger.setTrigger(new Table("audit")); + trigger.setTable(new Table("users")); + trigger.setTiming(CreateTrigger.Timing.AFTER); + trigger.setEvent(CreateTrigger.Event.INSERT); + Function routine = new Function(); + routine.setName("audit_row"); + routine.setParameters(new ExpressionList<>()); + trigger.setRoutine(routine); + assertThat(CCJSqlParserUtil.parse(trigger.toString()).toString()) + .isEqualTo(trigger.toString()); + CreateRole role = new CreateRole(); + role.setName("reader"); + role.getOptions().add(new RoleOption(RoleOption.Kind.NOLOGIN)); + assertThat(CCJSqlParserUtil.parse(role.toString()).toString()) + .isEqualTo("CREATE ROLE reader NOLOGIN"); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE OR REPLACE ROLE app", "CREATE ROLE app WITH", "CREATE ROLE app CONNECTION 10", + "CREATE ROLE app PASSWORD", "ALTER ROLE ALL LOGIN", + "ALTER ROLE app IN DATABASE appdb LOGIN", + "ALTER ROLE app IN ROLE reader", "ALTER GROUP staff LOGIN", + "GRANT SELECT ON users FROM app", "REVOKE SELECT ON users TO app", + "GRANT SELECT () ON users TO app", "GRANT SELECT ON users TO app WITH ADMIN OPTION", + "GRANT reader TO app WITH GRANT OPTION", + "REVOKE ADMIN OPTION FOR SELECT ON users FROM app", + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLE users TO app", + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app GRANTED BY owner", + "CREATE TRIGGER audit BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION audit_row(id + 1)", + "CREATE CONSTRAINT TRIGGER audit BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION audit_row()", + "CREATE TRIGGER audit AFTER INSERT ON users DEFERRABLE EXECUTE FUNCTION audit_row()", + "CREATE OR REPLACE CONSTRAINT TRIGGER audit AFTER INSERT ON users FOR EACH ROW EXECUTE FUNCTION audit_row()" + }) + void testInvalidSyntax(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessMethodTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessMethodTest.java new file mode 100644 index 0000000000..58b6ddc3cc --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessMethodTest.java @@ -0,0 +1,85 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlAccessMethodTest { + @ParameterizedTest + @ValueSource(strings = {"TABLE", "INDEX"}) + void preservesAccessMethodKindAndDoesNotInventTableReferences(String type) + throws JSQLParserException { + String sql = "CREATE ACCESS METHOD am TYPE " + type + " HANDLER public.handlerfn"; + CreateAccessMethod statement = assertInstanceOf(CreateAccessMethod.class, + CCJSqlParserUtil.parse(sql)); + assertEquals("am", statement.getName()); + assertEquals(CreateAccessMethod.Type.valueOf(type), statement.getType()); + assertEquals("public.handlerfn", statement.getHandler()); + assertEquals(sql, statement.toString()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + assertTrue(statement.getFeatures().modifiesSchema()); + assertFalse(statement.getFeatures().returnsResultSet()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(sql, output.toString()); + assertInstanceOf(CreateAccessMethod.class, CCJSqlParserUtil.parse(output.toString())); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + assertTrue(new Validation(List.of(PostgresqlVersion.V12), sql).validate().isEmpty()); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), sql).validate().isEmpty()); + assertEquals(type.equals("INDEX"), + new Validation(List.of(PostgresqlVersion.V10), sql).validate().isEmpty()); + } + + @Test + void quotedNamesRemainMutableAndVisitorsReceiveContext() throws JSQLParserException { + CreateAccessMethod statement = assertInstanceOf(CreateAccessMethod.class, + CCJSqlParserUtil.parse( + "CREATE ACCESS METHOD \"my method\" TYPE TABLE HANDLER \"my schema\".\"my handler\"")); + assertEquals("\"my method\"", statement.getName()); + assertEquals("\"my schema\".\"my handler\"", statement.getHandler()); + String actual = statement.accept(new StatementVisitorAdapter() { + @Override + public String visit(CreateAccessMethod method, S context) { + return context + ":" + method.getName(); + } + }, "visited"); + assertEquals("visited:\"my method\"", actual); + statement.setName("other_method"); + statement.setType(CreateAccessMethod.Type.INDEX); + statement.setHandler("other_schema.handler"); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals("CREATE ACCESS METHOD other_method TYPE INDEX HANDLER other_schema.handler", + output.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE ACCESS METHOD am TYPE TABLE", + "CREATE ACCESS METHOD am TYPE VIEW HANDLER fn", + "CREATE OR REPLACE ACCESS METHOD am TYPE TABLE HANDLER fn", + "CREATE ACCESS METHOD am TYPE TABLE HANDLER fn()"}) + void rejectsIncompleteOrInvalidAccessMethodDefinitions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCollationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCollationTest.java new file mode 100644 index 0000000000..8f67fb6bd5 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCollationTest.java @@ -0,0 +1,87 @@ +/*- + * #%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.statement.create; + +import net.sf.jsqlparser.statement.create.table.IndexOption; +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlCollationTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE COLLATION c FROM \"C\"", + "CREATE COLLATION IF NOT EXISTS app.c FROM pg_catalog.\"C\"", + "CREATE COLLATION c (locale='C')", + "CREATE COLLATION c (locale='C',deterministic)", + "CREATE COLLATION c (provider=builtin,locale='C.UTF-8')", + "CREATE COLLATION c (lc_collate='C',lc_ctype='C',provider=libc)", + "CREATE COLLATION c (provider=icu,locale='und',deterministic=false,rules='&V << w',version='1')", + "ALTER COLLATION app.c REFRESH VERSION", "ALTER COLLATION app.c RENAME TO c2", + "ALTER COLLATION c OWNER TO CURRENT_USER", "ALTER COLLATION c SET SCHEMA app"}) + void structuredDefinitionsAndChangesRoundTrip(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertTrue(statement instanceof CreateCollation || statement instanceof AlterCollation); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + } + + @Test + void switchingBetweenCopyAndOptionsClearsInactiveState() throws JSQLParserException { + CreateCollation statement = + (CreateCollation) CCJSqlParserUtil.parse("CREATE COLLATION c FROM \"C\""); + statement.setOptions( + new ArrayList<>(List.of(new IndexOption("locale", new StringValue("C"), true)))); + assertNull(statement.getSourceCollation()); + statement.getOptions().get(0).setValue(new StringValue("C.UTF-8")); + List expressions = new ArrayList<>(); + statement.visitExpressions(expressions::add); + assertEquals(1, expressions.size()); + roundTrip(statement); + statement.setSourceCollation("pg_catalog.\"C\""); + assertNull(statement.getOptions()); + assertEquals("CREATE COLLATION c FROM pg_catalog.\"C\"", statement.toString()); + expressions.clear(); + statement.visitExpressions(expressions::add); + assertTrue(expressions.isEmpty()); + roundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE COLLATION c", "CREATE COLLATION c ()", + "CREATE COLLATION c (locale=)", "CREATE COLLATION c (locale 'C')", + "CREATE OR REPLACE COLLATION c FROM \"C\"", "ALTER COLLATION c REFRESH", + "ALTER COLLATION c RENAME TO c2, SET SCHEMA app"}) + void rejectsIncompleteDefinitions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnIndexOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnIndexOptionsTest.java new file mode 100644 index 0000000000..a8f457a042 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnIndexOptionsTest.java @@ -0,0 +1,115 @@ +/*- + * #%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.statement.create; + +import net.sf.jsqlparser.statement.create.table.IndexOption; +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlColumnIndexOptionsTest { + @ParameterizedTest + @ValueSource(strings = {"PRIMARY KEY", "UNIQUE", "UNIQUE NULLS NOT DISTINCT"}) + void columnStorageAndTablespaceStayOnTheConstraint(String kind) throws JSQLParserException { + for (boolean alter : new boolean[] {false, true}) { + String prefix = alter ? "ALTER TABLE t ADD COLUMN " : "CREATE TABLE t ("; + Statement statement = parse(prefix + "id INT CONSTRAINT uq " + kind + + " WITH (fillfactor=70) USING INDEX TABLESPACE old_space DEFERRABLE INITIALLY DEFERRED" + + (alter ? "" : ")")); + ColumnDefinition column = alter + ? ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0) + : ((CreateTable) statement).getColumnDefinitions().get(0); + assertEquals(1, column.getColumnOptions().size()); + ColumnOption option = column.getColumnOptions().get(0); + assertEquals(ColumnOption.Kind.CONSTRAINT, option.getKind()); + KeyConstraint key = (KeyConstraint) option.getConstraint(); + assertEquals("uq", key.getName()); + assertEquals("old_space", key.getTableSpace()); + assertEquals("70", key.getStorageParameters().get(0).getValue().toString()); + assertNotNull(key.getConstraintAttributes()); + key.setStorageParameters( + List.of(new IndexOption("fillfactor", new LongValue(80), true))); + key.setTableSpace("new_space"); + String sql = statement.toString(); + assertTrue(sql.contains("WITH (fillfactor = 80) USING INDEX TABLESPACE new_space")); + assertFalse(sql.contains("70")); + assertFalse(sql.contains("old_space")); + assertRoundTrip(statement); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (id INT) WITH (autovacuum_enabled,toast.autovacuum_enabled=false)", + "CREATE TABLE t (id INT PRIMARY KEY WITH (deduplicate_items))", + "CREATE TABLE t (id INT, CONSTRAINT uq UNIQUE(id) WITH (deduplicate_items))", + "ALTER TABLE t ADD CONSTRAINT uq UNIQUE(id) WITH (deduplicate_items)", + "CREATE INDEX ix ON t(id) WITH (deduplicate_items)", + "CREATE INDEX ix ON t USING gin(tags) WITH (fastupdate)", + "CREATE MATERIALIZED VIEW mv WITH (autovacuum_enabled) AS SELECT * FROM t"}) + void bareStorageOptionsRoundTrip(String sql) throws JSQLParserException { + assertRoundTrip(parse(sql)); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void bareOptionsCanBeAssignedAndCleared() throws JSQLParserException { + CreateIndex index = (CreateIndex) parse("CREATE INDEX ix ON t(id) WITH(deduplicate_items)"); + IndexOption option = index.getStorageParameters().get(0); + assertNull(option.getValue()); + assertFalse(option.isUseEquals()); + option.setName("fillfactor"); + option.setValue(new LongValue(80)); + option.setUseEquals(true); + assertTrue(index.toString().contains("WITH (fillfactor = 80)")); + assertRoundTrip(index); + option.setName("deduplicate_items"); + option.setValue(null); + assertTrue(index.toString().contains("WITH (deduplicate_items)")); + assertRoundTrip(index); + } + + @Test + void columnIncludeAndIncompleteParametersAreRejected() { + for (String sql : new String[] { + "CREATE TABLE t(id INT UNIQUE INCLUDE(label),label TEXT)", + "CREATE TABLE t(id INT) WITH (fillfactor=)", + "CREATE TABLE t(id INT) WITH (fillfactor 70)", + "CREATE INDEX ix ON t(id int4_ops(deduplicate_items))"}) { + assertThrows(JSQLParserException.class, () -> parse(sql), sql); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnKeyMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnKeyMutationTest.java new file mode 100644 index 0000000000..736cadb9e2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnKeyMutationTest.java @@ -0,0 +1,91 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlColumnKeyMutationTest { + @ParameterizedTest + @ValueSource(strings = {"PRIMARY KEY", "UNIQUE", "UNIQUE NULLS NOT DISTINCT"}) + void keyNameAndAttributesAreOwnedByOneNode(String key) throws JSQLParserException { + for (boolean alter : new boolean[] {false, true}) { + String prefix = alter ? "ALTER TABLE t ADD COLUMN " : "CREATE TABLE t ("; + String suffix = alter ? "" : ")"; + Statement statement = parse(prefix + "id INT CONSTRAINT old_name " + key + + " DEFERRABLE INITIALLY DEFERRED NOT NULL" + suffix); + ColumnDefinition column = alter + ? ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0) + : ((CreateTable) statement).getColumnDefinitions().get(0); + assertEquals(2, column.getColumnOptions().size()); + NamedConstraint constraint = column.getColumnOptions().get(0).getConstraint(); + assertEquals("old_name", constraint.getName()); + assertTrue(constraint.getConstraintAttributes().getDeferrable()); + constraint.setName("new_name"); + constraint.getConstraintAttributes().setDeferrable(false); + constraint.getConstraintAttributes() + .setInitially(ConstraintAttributes.Initially.IMMEDIATE); + assertSql(statement, prefix + "id INT CONSTRAINT new_name " + key + + " NOT DEFERRABLE INITIALLY IMMEDIATE NOT NULL" + suffix); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id INT CONSTRAINT old_fk REFERENCES parent(id))", + "ALTER TABLE t ADD COLUMN id INT CONSTRAINT old_fk REFERENCES parent(id)"}) + void referenceNameCanBeReplacedAndRemoved(String sql) throws JSQLParserException { + Statement statement = parse(sql); + ForeignKeyReference reference = statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0).getForeignKeyReference() + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0) + .getForeignKeyReference(); + assertEquals("old_fk", reference.getConstraintName()); + reference.setConstraintName("new_fk"); + assertSql(statement, sql.replace("old_fk", "new_fk")); + reference.setConstraintName(null); + assertSql(statement, sql.replace("CONSTRAINT old_fk ", "")); + } + + @ParameterizedTest + @ValueSource(strings = {"NOT ENFORCED NO INHERIT", "NO INHERIT NOT ENFORCED", + "NOT VALID NO INHERIT NOT ENFORCED", "NOT ENFORCED NOT VALID NO INHERIT"}) + void tableCheckAllowsInheritanceAmongAttributes(String attributes) throws JSQLParserException { + Alter alter = (Alter) parse("ALTER TABLE t ADD CHECK (id > 0) " + attributes); + CheckConstraint check = + (CheckConstraint) alter.getAlterExpressions().get(0).getConstraint(); + assertTrue(check.isNoInherit()); + assertFalse(check.getEnforced()); + assertEquals(attributes.contains("NOT VALID"), + check.getConstraintAttributes().isNotValid()); + assertSql(alter, "ALTER TABLE t ADD CHECK (id > 0) NO INHERIT NOT ENFORCED" + + (attributes.contains("NOT VALID") ? " NOT VALID" : "")); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertSql(Statement statement, String expected) throws JSQLParserException { + assertEquals(expected, statement.toString()); + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(expected, buffer.toString()); + assertEquals(expected, parse(expected).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnNotNullTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnNotNullTest.java new file mode 100644 index 0000000000..1b2b52cdef --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnNotNullTest.java @@ -0,0 +1,100 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.NotNullConstraint; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlColumnNotNullTest { + @ParameterizedTest + @ValueSource(strings = {"NOT NULL NO INHERIT", "CONSTRAINT nn NOT NULL", + "CONSTRAINT nn NOT NULL NO INHERIT", "CONSTRAINT \"Named NN\" NOT NULL NO INHERIT"}) + void reusesNotNullConstraintsInCreateAndAlterColumns(String clause) throws JSQLParserException { + for (String prefix : List.of("CREATE TABLE t (", "ALTER TABLE t ADD COLUMN ")) { + Statement statement = parse(prefix + "id INT " + clause + " DEFAULT 7" + + (prefix.startsWith("CREATE") ? ", value INT)" : ", ADD COLUMN value INT")); + ColumnDefinition column = column(statement); + NotNullConstraint constraint = assertInstanceOf(NotNullConstraint.class, + column.getColumnOptions().get(0).getConstraint()); + assertTrue(constraint.isColumnConstraint()); + assertNull(constraint.getColumn()); + assertEquals(clause.endsWith("NO INHERIT"), constraint.isNoInherit()); + assertEquals(ColumnOption.Kind.DEFAULT, column.getColumnOptions().get(1).getKind()); + roundTrip(statement); + constraint.setName("replacement"); + constraint.setNoInherit(!constraint.isNoInherit()); + assertTrue(statement.toString().contains("CONSTRAINT replacement NOT NULL")); + roundTrip(statement); + } + } + + @Test + void retainsPlainNullabilityAndUsesTheExistingTableModel() throws JSQLParserException { + ColumnOption option = + column(parse("CREATE TABLE t (id INT NOT NULL)")).getColumnOptions().get(0); + assertEquals(ColumnOption.Kind.NULLABILITY, option.getKind()); + assertEquals(false, option.getNullable()); + NotNullConstraint table = (NotNullConstraint) ((CreateTable) parse( + "CREATE TABLE t (id INT, CONSTRAINT nn NOT NULL id NO INHERIT)")) + .getTableConstraints() + .get(0); + assertFalse(table.isColumnConstraint()); + assertEquals("id", table.getColumn().getColumnName()); + assertEquals("CONSTRAINT nn NOT NULL NO INHERIT", new NotNullConstraint() + .withColumnConstraint(true).withName("nn").withNoInherit(true).toString()); + } + + @Test + void doesNotInventASecondColumnExpressionForAnImplicitTarget() throws JSQLParserException { + CreateTable table = + (CreateTable) parse("CREATE TABLE t (id INT DEFAULT 7 NOT NULL NO INHERIT)"); + List expressions = new ArrayList<>(); + TableDefinitionTraversal.visit(table, expressions::add, t -> { + }); + assertEquals(1, expressions.size()); + assertEquals("7", expressions.get(0).toString()); + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + private static ColumnDefinition column(Statement statement) { + return statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java new file mode 100644 index 0000000000..5946470e7c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java @@ -0,0 +1,109 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.Arrays; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlColumnTypeOptionsTest { + @ParameterizedTest + @ValueSource(strings = {"INT", "TEXT", "NUMERIC(10,2)", "TIMESTAMP(3) WITH TIME ZONE", + "DOUBLE PRECISION", "public.mood"}) + void arrayKeywordBelongsToDataType(String type) throws JSQLParserException { + for (boolean alter : new boolean[] {false, true}) { + for (String dimensions : new String[] {"", "[4]"}) { + Statement statement = parse(definition(alter, "a " + type + " ARRAY" + dimensions)); + ColumnDefinition column = column(statement); + ColDataType dataType = column.getColDataType(); + assertTrue(dataType.isUsingArrayKeyword()); + assertEquals(dimensions.isEmpty() ? Arrays.asList((Integer) null) : List.of(4), + dataType.getArrayData()); + if (alter) { + assertTrue(column.getColumnOptions().isEmpty()); + } else { + assertNull(column.getColumnOptions()); + } + dataType.setArrayData(List.of(8)); + assertTrue(statement.toString().contains(" ARRAY[8]")); + assertRoundTrip(statement); + dataType.setUsingArrayKeyword(false); + assertTrue(statement.toString().contains("[8]")); + assertFalse(statement.toString().contains(" ARRAY")); + assertRoundTrip(statement); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"PLAIN", "EXTERNAL", "EXTENDED", "MAIN", "DEFAULT"}) + void storageAndCompressionAreSharedByCreateAndAddColumn(String storage) + throws JSQLParserException { + for (boolean alter : new boolean[] {false, true}) { + Statement statement = parse(definition(alter, + "a TEXT STORAGE " + storage + " COMPRESSION pglz COLLATE \"C\" DEFAULT 'x'")); + List options = column(statement).getColumnOptions(); + assertEquals(ColumnOption.Storage.valueOf(storage), options.get(0).getStorage()); + assertEquals("pglz", options.get(1).getCompression()); + options.get(0).setStorage(ColumnOption.Storage.EXTENDED); + options.get(1).setCompression("default"); + assertTrue(statement.toString().contains("STORAGE EXTENDED COMPRESSION default")); + assertRoundTrip(statement); + options.remove(0); + assertFalse(statement.toString().contains("STORAGE")); + assertRoundTrip(statement); + } + } + + @Test + void bracketArraysAndMysqlColumnStorageRemainValid() throws JSQLParserException { + CreateTable table = (CreateTable) parse("CREATE TABLE t (a INT[3][4], b TEXT[])"); + assertEquals(List.of(3, 4), + table.getColumnDefinitions().get(0).getColDataType().getArrayData()); + assertFalse(table.getColumnDefinitions().get(0).getColDataType().isUsingArrayKeyword()); + for (String value : new String[] {"DISK", "MEMORY"}) { + Statement statement = + CCJSqlParserUtil.parse("CREATE TABLE t (a INT STORAGE " + value + ")", + p -> p.withDialect(Dialect.MYSQL)); + assertTrue(statement.toString().contains("STORAGE " + value)); + } + } + + private static String definition(boolean alter, String column) { + return alter ? "ALTER TABLE t ADD COLUMN " + column : "CREATE TABLE t (" + column + ")"; + } + + private static ColumnDefinition column(Statement statement) { + return statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlConstraintEnforcementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlConstraintEnforcementTest.java new file mode 100644 index 0000000000..298d02bfcd --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlConstraintEnforcementTest.java @@ -0,0 +1,135 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlConstraintEnforcementTest { + @ParameterizedTest + @ValueSource(strings = {"ENFORCED", "NOT ENFORCED", + "DEFERRABLE INITIALLY DEFERRED NOT ENFORCED", + "NOT ENFORCED DEFERRABLE INITIALLY DEFERRED", + "INITIALLY IMMEDIATE NOT DEFERRABLE ENFORCED"}) + void sharesForeignKeyAttributesAcrossCreateAndAlter(String attributes) + throws JSQLParserException { + for (String prefix : List.of("CREATE TABLE t (id INT, ", "ALTER TABLE t ADD ")) { + Statement statement = + parse(prefix + "CONSTRAINT fk FOREIGN KEY (id) REFERENCES public.p (id) " + + "MATCH SIMPLE ON DELETE CASCADE " + attributes + + (prefix.startsWith("CREATE") ? ")" : ", ADD COLUMN extra INT")); + ForeignKeyConstraint fk = (ForeignKeyConstraint) (statement instanceof CreateTable + ? ((CreateTable) statement).getTableConstraints().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getConstraint()); + assertEquals(!attributes.contains("NOT ENFORCED"), + fk.getConstraintAttributes().getEnforced()); + roundTrip(statement); + fk.getConstraintAttributes().setEnforced(true); + roundTrip(statement); + assertFalse(statement.toString().contains("NOT ENFORCED")); + fk.getConstraintAttributes().setEnforced(null); + roundTrip(statement); + assertFalse(statement.toString().contains("ENFORCED")); + } + } + + @ParameterizedTest + @ValueSource(strings = {"ENFORCED", "NOT ENFORCED DEFERRABLE INITIALLY DEFERRED"}) + void columnReferencesRetainTheirOwnAttributesAndFollowingOptions(String attributes) + throws JSQLParserException { + for (String prefix : List.of("CREATE TABLE t (", "ALTER TABLE t ADD COLUMN ")) { + Statement statement = parse(prefix + "id INT REFERENCES public.p (id) " + attributes + + " NOT NULL" + + (prefix.startsWith("CREATE") ? ", value INT)" : ", ADD COLUMN value INT")); + ColumnDefinition column = statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0); + ForeignKeyReference reference = + column.getColumnOptions().get(0).getForeignKeyReference(); + assertNotNull(reference.getConstraintAttributes()); + assertEquals(!attributes.contains("NOT ENFORCED"), + reference.getConstraintAttributes().getEnforced()); + assertEquals(ColumnOption.Kind.NULLABILITY, column.getColumnOptions().get(1).getKind()); + roundTrip(statement); + reference.getConstraintAttributes().setEnforced(null); + roundTrip(statement); + } + } + + @Test + void checkLegacyAccessorsUseTheSharedAttributeState() throws JSQLParserException { + for (String prefix : List.of("CREATE TABLE t (id INT, ", "ALTER TABLE t ADD ")) { + Statement statement = parse(prefix + "CHECK (id > 0) NOT ENFORCED" + + (prefix.startsWith("CREATE") ? ")" : " NOT VALID")); + CheckConstraint check = (CheckConstraint) (statement instanceof CreateTable + ? ((CreateTable) statement).getTableConstraints().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getConstraint()); + assertEquals(false, check.getConstraintAttributes().getEnforced()); + check.getConstraintAttributes().setEnforced(true); + assertEquals(true, check.getEnforced()); + check.setEnforced(false); + assertEquals(false, check.getConstraintAttributes().getEnforced()); + roundTrip(statement); + check.setEnforced(null); + assertNull(check.getEnforced()); + roundTrip(statement); + } + assertNull(new CheckConstraint().withEnforced(null).getConstraintAttributes()); + } + + @Test + void preservesAlterEnforcementAndStatementBoundaries() throws JSQLParserException { + for (String flag : List.of("ENFORCED", "NOT ENFORCED")) { + Alter alter = (Alter) parse("ALTER TABLE t ALTER CONSTRAINT fk " + flag); + assertEquals(!flag.startsWith("NOT"), alter.getAlterExpressions().get(0).isEnforced()); + roundTrip(alter); + } + assertEquals(2, CCJSqlParserUtil.parseStatements( + "CREATE TABLE t(id INT REFERENCES p NOT ENFORCED); SELECT 1").size()); + } + + @ParameterizedTest + @ValueSource(strings = {"FOREIGN KEY(id) REFERENCES p ENFORCED NOT ENFORCED", + "FOREIGN KEY(id) REFERENCES p DEFERRABLE NOT DEFERRABLE", + "FOREIGN KEY(id) REFERENCES p ENFORCED INITIALLY wrong", + "PRIMARY KEY(id) NOT ENFORCED", "CHECK(id > 0) ENFORCED ENFORCED"}) + void rejectsMalformedAttributeTails(String constraint) { + assertThrows(JSQLParserException.class, + () -> parse("CREATE TABLE t(id INT, " + constraint + ")")); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t(id INT REFERENCES p ENFORCED NOT VALID)", + "ALTER TABLE t ADD COLUMN id INT REFERENCES p NOT VALID"}) + void rejectsNotValidOnColumnReferences(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCreateViewTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCreateViewTest.java new file mode 100644 index 0000000000..524c3d61fc --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCreateViewTest.java @@ -0,0 +1,112 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.BooleanValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.statement.create.view.ViewOption; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * @see PostgreSQL CREATE VIEW + */ +class PostgreSqlCreateViewTest { + @Test + void testSecurityOptions() throws JSQLParserException { + CreateView view = (CreateView) assertSqlCanBeParsedAndDeparsed( + "CREATE VIEW visible_accounts WITH (security_barrier = true, security_invoker = false)" + + " AS SELECT * FROM accounts WHERE enabled WITH LOCAL CHECK OPTION"); + assertThat(view.getOptions()).hasSize(2); + assertThat(view.getOptions().get(0).getKind()).isEqualTo(ViewOption.Kind.SECURITY_BARRIER); + assertThat(view.getOptions().get(0).getBooleanValue()).isTrue(); + assertThat(view.getOptions().get(1).getBooleanValue()).isFalse(); + assertThat(view.getCheckOption()).isEqualTo(CreateView.CheckOption.LOCAL); + assertThat(TablesNamesFinder.findTables(view.toString())) + .containsExactlyInAnyOrder("visible_accounts", "accounts"); + } + + @Test + void testImplicitOptionValueAndCheckOptionParameter() throws JSQLParserException { + CreateView view = (CreateView) assertSqlCanBeParsedAndDeparsed( + "CREATE VIEW visible_accounts WITH (security_barrier, check_option = 'local')" + + " AS SELECT * FROM accounts"); + assertThat(view.getOptions().get(0).getValue()).isNull(); + assertThat(view.getOptions().get(0).getBooleanValue()).isTrue(); + assertThat(view.getOptions().get(1).getCheckOption()) + .isEqualTo(CreateView.CheckOption.LOCAL); + assertThat(view.getCheckOption()).isNull(); + } + + @Test + void testDefaultCheckOption() throws JSQLParserException { + CreateView view = (CreateView) assertSqlCanBeParsedAndDeparsed( + "CREATE VIEW visible_accounts AS SELECT * FROM accounts WITH CHECK OPTION"); + assertThat(view.getCheckOption()).isEqualTo(CreateView.CheckOption.DEFAULT); + assertThat(view.getEffectiveCheckOption()).isEqualTo(CreateView.CheckOption.CASCADED); + } + + @Test + void testRecursiveView() throws JSQLParserException { + CreateView view = (CreateView) assertSqlCanBeParsedAndDeparsed( + "CREATE RECURSIVE VIEW numbers(n) AS VALUES (1)" + + " UNION ALL SELECT n + 1 FROM numbers WHERE n < 5"); + assertThat(view.isRecursive()).isTrue(); + assertThat(view.getColumnNames()).hasSize(1); + } + + @Test + void testMaterializedViewParameters() throws JSQLParserException { + CreateView view = (CreateView) assertSqlCanBeParsedAndDeparsed( + "CREATE MATERIALIZED VIEW IF NOT EXISTS account_totals(id) USING heap" + + " WITH (fillfactor = 70) TABLESPACE archive" + + " AS SELECT id FROM accounts WITH NO DATA"); + assertThat(view.isMaterialized()).isTrue(); + assertThat(view.isIfNotExists()).isTrue(); + assertThat(view.isIfNotExistsAfterViewName()).isFalse(); + assertThat(view.getAccessMethod()).isEqualTo("heap"); + assertThat(view.getStorageParameters().get(0).getName()).isEqualTo("fillfactor"); + assertThat(view.getTableSpace()).isEqualTo("archive"); + assertThat(view.getWithData()).isFalse(); + assertThat(view.getOptions()).isNull(); + } + + @ParameterizedTest + @ValueSource(strings = {"", " WITH DATA", " WITH NO DATA"}) + void testMaterializedViewDataMode(String suffix) throws JSQLParserException { + CreateView view = (CreateView) assertSqlCanBeParsedAndDeparsed( + "CREATE MATERIALIZED VIEW account_totals AS SELECT * FROM accounts" + suffix); + assertThat(view.getWithData()).isEqualTo(suffix.isEmpty() ? null : !suffix.contains("NO")); + CreateView reparsed = (CreateView) CCJSqlParserUtil.parse(view.toString()); + assertThat(reparsed.getWithData()).isEqualTo(view.getWithData()); + } + + @Test + void testProgrammaticConstruction() throws JSQLParserException { + CreateView view = new CreateView().withView(new Table("visible_accounts")) + .withSelect((Select) CCJSqlParserUtil.parse("SELECT * FROM accounts")); + view.setOptions(Collections.singletonList( + new ViewOption("security_barrier", new BooleanValue(true), true))); + view.setCheckOption(CreateView.CheckOption.CASCADED); + CreateView parsed = (CreateView) assertSqlCanBeParsedAndDeparsed(view.toString()); + assertThat(parsed.getOptions().get(0).getBooleanValue()).isTrue(); + assertThat(parsed.getCheckOption()).isEqualTo(CreateView.CheckOption.CASCADED); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlEventTriggerTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlEventTriggerTest.java new file mode 100644 index 0000000000..83810d69a9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlEventTriggerTest.java @@ -0,0 +1,183 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; +import net.sf.jsqlparser.statement.alter.RelationAlterAction.EnableState; +import net.sf.jsqlparser.statement.create.event.AlterEvent; +import net.sf.jsqlparser.statement.create.event.CreateEvent; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlEventTriggerTest { + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + private static T roundTrip(String sql, Class type) + throws JSQLParserException { + T statement = + assertInstanceOf(type, assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL)); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), POSTGRESQL).toString()); + return statement; + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE EVENT TRIGGER etg ON ddl_command_start WHEN TAG IN ('CREATE TABLE') " + + "EXECUTE FUNCTION abort_any_command()", + "CREATE EVENT TRIGGER etg ON sql_drop EXECUTE FUNCTION f()", + "CREATE EVENT TRIGGER \"On Login\" ON login EXECUTE FUNCTION app.on_login()", + "CREATE EVENT TRIGGER etg ON table_rewrite EXECUTE PROCEDURE f()"}) + void parsesEventTriggerDefinitions(String sql) throws JSQLParserException { + roundTrip(sql, CreateEventTrigger.class); + assertInstanceOf(CreateEventTrigger.class, CCJSqlParserUtil.parse(sql)); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", POSTGRESQL).size()); + } + + @Test + void keepsEventFiltersAndFunction() throws JSQLParserException { + CreateEventTrigger trigger = roundTrip("CREATE EVENT TRIGGER etg ON ddl_command_end " + + "WHEN TAG IN ('CREATE TABLE', 'DROP TABLE') AND tag IN ('ALTER TABLE') " + + "EXECUTE PROCEDURE audit.log_ddl()", CreateEventTrigger.class); + + assertEquals("etg", trigger.getName()); + assertEquals("ddl_command_end", trigger.getEvent()); + assertEquals(2, trigger.getFilters().size()); + CreateEventTrigger.Filter first = trigger.getFilters().get(0); + assertEquals("TAG", first.getVariable()); + assertEquals(List.of("CREATE TABLE", "DROP TABLE"), first.getValues().stream() + .map(StringValue::getValue).collect(Collectors.toList())); + assertEquals("tag", trigger.getFilters().get(1).getVariable()); + assertEquals(CreateTrigger.ExecuteKeyword.PROCEDURE, trigger.getExecuteKeyword()); + assertEquals("audit.log_ddl", trigger.getFunction().getName()); + assertTrue(trigger.getFunction().getParameters().isEmpty()); + } + + @Test + void buildsAnEventTrigger() { + CreateEventTrigger trigger = new CreateEventTrigger().withName("etg") + .withEvent("ddl_command_start") + .withFunction(new Function().withName("guard") + .withParameters(new ExpressionList<>())) + .addFilters(new CreateEventTrigger.Filter("TAG", + List.of(new StringValue("DROP TABLE")))); + + assertDeparse(trigger, "CREATE EVENT TRIGGER etg ON ddl_command_start " + + "WHEN TAG IN ('DROP TABLE') EXECUTE FUNCTION guard()"); + } + + @ParameterizedTest + @ValueSource(strings = {"DISABLE", "ENABLE", "ENABLE REPLICA", "ENABLE ALWAYS"}) + void alterEventTriggerSharesTheEnableStates(String state) throws JSQLParserException { + AlterEventTrigger alter = + roundTrip("ALTER EVENT TRIGGER etg " + state, AlterEventTrigger.class); + + assertEquals("etg", alter.getName()); + assertEquals(AlterEventTrigger.Action.STATE, alter.getAction()); + assertEquals(EnableState.valueOf(state.replace(' ', '_')), alter.getEnableState()); + } + + @Test + void alterEventTriggerOwnerAndName() throws JSQLParserException { + AlterEventTrigger owner = + roundTrip("ALTER EVENT TRIGGER etg OWNER TO CURRENT_USER", AlterEventTrigger.class); + assertEquals(AlterEventTrigger.Action.OWNER, owner.getAction()); + assertEquals("CURRENT_USER", owner.getOwner()); + + AlterEventTrigger rename = + roundTrip("ALTER EVENT TRIGGER etg RENAME TO etg2", AlterEventTrigger.class); + assertEquals(AlterEventTrigger.Action.RENAME, rename.getAction()); + assertEquals("etg2", rename.getNewName()); + } + + @Test + void settersSelectTheAction() { + AlterEventTrigger alter = new AlterEventTrigger().withName("etg").withOwner("admin"); + alter.setNewName("etg2"); + assertEquals(AlterEventTrigger.Action.RENAME, alter.getAction()); + assertNull(alter.getOwner()); + assertDeparse(alter, "ALTER EVENT TRIGGER etg RENAME TO etg2"); + + alter.setEnableState(EnableState.ENABLE_ALWAYS); + assertNull(alter.getNewName()); + assertDeparse(alter, "ALTER EVENT TRIGGER etg ENABLE ALWAYS"); + } + + @Test + void mySqlEventsNamedTriggerStayEvents() throws JSQLParserException { + Consumer mysql = parser -> parser.withDialect(Dialect.MYSQL); + assertInstanceOf(CreateEvent.class, CCJSqlParserUtil.parse( + "CREATE EVENT trigger ON SCHEDULE EVERY 1 DAY DO SELECT 1", mysql)); + assertInstanceOf(AlterEvent.class, + CCJSqlParserUtil.parse("ALTER EVENT trigger RENAME TO e2", mysql)); + assertInstanceOf(AlterEvent.class, + CCJSqlParserUtil.parse("ALTER EVENT trigger ENABLE", mysql)); + assertInstanceOf(AlterEvent.class, CCJSqlParserUtil.parse("ALTER EVENT e DISABLE", mysql)); + } + + @Test + void visitorsValidationAndFeatures() throws JSQLParserException { + String create = "CREATE EVENT TRIGGER etg ON ddl_command_start EXECUTE FUNCTION f()"; + String alter = "ALTER EVENT TRIGGER etg DISABLE"; + for (String sql : List.of(create, alter)) { + Statement statement = CCJSqlParserUtil.parse(sql, POSTGRESQL); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertTrue(new Validation(List.of(PostgresqlVersion.V10), sql).validate().isEmpty()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + assertThat(new Validation(List.of(MySqlVersion.V8_0), sql).validate()).isNotEmpty(); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE EVENT TRIGGER etg ON ddl_command_start EXECUTE FUNCTION f(1)", + "CREATE EVENT TRIGGER etg ON ddl_command_start EXECUTE FUNCTION f", + "CREATE OR REPLACE EVENT TRIGGER etg ON ddl_command_start EXECUTE FUNCTION f()", + "CREATE EVENT TRIGGER etg ON ddl_command_start WHEN TAG IN () EXECUTE FUNCTION f()", + "CREATE EVENT TRIGGER etg ON ddl_command_start WHEN TAG IN (1) EXECUTE FUNCTION f()", + "CREATE EVENT TRIGGER etg ON ddl_command_start WHEN TAG = 'x' EXECUTE FUNCTION f()", + "ALTER EVENT TRIGGER etg", + "ALTER EVENT TRIGGER etg DISABLE ALWAYS", + "ALTER EVENT TRIGGER etg OWNER admin"}) + void rejectsInvalidEventTriggers(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, POSTGRESQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java new file mode 100644 index 0000000000..e7eef7d45a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java @@ -0,0 +1,275 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; +import net.sf.jsqlparser.statement.alter.ForeignObjectAlterAction; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.statement.drop.DropUserMapping; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlForeignDataObjectsTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE FOREIGN DATA WRAPPER w", + "CREATE FOREIGN DATA WRAPPER w NO HANDLER NO VALIDATOR OPTIONS(x 'y')", + "CREATE FOREIGN DATA WRAPPER w HANDLER public.handler VALIDATOR public.validator", + "CREATE FOREIGN DATA WRAPPER w VALIDATOR public.validator HANDLER public.handler", + "ALTER FOREIGN DATA WRAPPER w OPTIONS(ADD x 'y')", + "ALTER FOREIGN DATA WRAPPER w NO HANDLER NO VALIDATOR", + "ALTER FOREIGN DATA WRAPPER w HANDLER public.handler OPTIONS(SET x 'z',DROP y)", + "ALTER FOREIGN DATA WRAPPER w OWNER TO CURRENT_USER", + "ALTER FOREIGN DATA WRAPPER w RENAME TO w2", + "CREATE SERVER s FOREIGN DATA WRAPPER w OPTIONS(host 'localhost')", + "CREATE SERVER IF NOT EXISTS s TYPE 'postgres' VERSION '18' FOREIGN DATA WRAPPER w", + "CREATE SERVER s VERSION NULL FOREIGN DATA WRAPPER w", + "ALTER SERVER s OPTIONS(SET host '127.0.0.1')", "ALTER SERVER s VERSION NULL", + "ALTER SERVER s VERSION '18' OPTIONS(ADD port '5432',DROP host)", + "ALTER SERVER s OWNER TO CURRENT_ROLE", "ALTER SERVER s RENAME TO s2", + "CREATE USER MAPPING FOR CURRENT_USER SERVER s OPTIONS(user 'u')", + "CREATE USER MAPPING IF NOT EXISTS FOR PUBLIC SERVER s", + "CREATE USER MAPPING FOR USER SERVER s OPTIONS(user 'u',password 'a,b')", + "CREATE USER MAPPING FOR CURRENT_ROLE SERVER s", + "CREATE USER MAPPING FOR \"user name\" SERVER \"server name\"", + "ALTER USER MAPPING FOR CURRENT_USER SERVER s OPTIONS(SET \"user\" 'u2')", + "ALTER USER MAPPING FOR PUBLIC SERVER s OPTIONS(DROP password,ADD user 'u')"}) + void foreignObjectsShareOptionsAndKeepBoundaries(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertInstanceOf(ForeignDataStatement.class, statement); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), sql).validate().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP FOREIGN DATA WRAPPER w", + "DROP FOREIGN DATA WRAPPER IF EXISTS w1, w2 CASCADE", + "DROP FOREIGN DATA WRAPPER w RESTRICT", + "DROP USER MAPPING FOR CURRENT_USER SERVER s", + "DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER s", + "DROP USER MAPPING FOR USER SERVER s", + "DROP USER MAPPING FOR \"user name\" SERVER \"server name\""}) + void dropsForeignDataObjects(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertEquals(sql, statement.toString()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), sql).validate().isEmpty()); + } + + @Test + void dropStatementsKeepObjectKindAndMappingKey() throws JSQLParserException { + Drop wrapper = (Drop) CCJSqlParserUtil.parse( + "DROP FOREIGN DATA WRAPPER IF EXISTS w1, w2 CASCADE"); + assertEquals(Drop.ObjectType.FOREIGN_DATA_WRAPPER, wrapper.getObjectType()); + assertEquals(List.of("w1", "w2"), + wrapper.getNames().stream().map(Table::getFullyQualifiedName) + .collect(Collectors.toList())); + assertTrue(wrapper.isIfExists()); + assertEquals(CascadeBehavior.CASCADE, wrapper.getBehavior()); + + DropUserMapping mapping = (DropUserMapping) CCJSqlParserUtil.parse( + "DROP USER MAPPING IF EXISTS FOR CURRENT_USER SERVER s"); + assertTrue(mapping.isIfExists()); + assertEquals("CURRENT_USER", mapping.getUser()); + assertEquals("s", mapping.getServer()); + mapping.setIfExists(false); + mapping.setServer("s2"); + assertEquals("DROP USER MAPPING FOR CURRENT_USER SERVER s2", mapping.toString()); + roundTrip(mapping); + + // MAPPING followed by neither FOR nor IF is still a user name + Drop user = (Drop) CCJSqlParserUtil.parse("DROP USER mapping"); + assertEquals(Drop.ObjectType.USER, user.getObjectType()); + } + + @Test + void omittedAndRemovedHandlersStayDistinct() throws JSQLParserException { + CreateForeignDataWrapper create = (CreateForeignDataWrapper) CCJSqlParserUtil.parse( + "CREATE FOREIGN DATA WRAPPER w"); + assertFalse(create.getFunctions().isHandlerSpecified()); + create.getFunctions().setHandler(null); + assertTrue(create.getFunctions().isHandlerSpecified()); + assertEquals("CREATE FOREIGN DATA WRAPPER w NO HANDLER", create.toString()); + create.getFunctions().setHandler("public.handler"); + roundTrip(create); + create.getFunctions().clearHandler(); + assertEquals("CREATE FOREIGN DATA WRAPPER w", create.toString()); + AlterForeignDataWrapper alter = (AlterForeignDataWrapper) CCJSqlParserUtil.parse( + "ALTER FOREIGN DATA WRAPPER w OPTIONS(SET x 'y')"); + alter.getOptions().get(0).setValue(new StringValue("changed")); + assertTrue(alter.toString().contains("SET x 'changed'")); + roundTrip(alter); + } + + @Test + void serverLiteralsAndOptionsAreVisitedAndMutable() throws JSQLParserException { + CreateServer create = (CreateServer) CCJSqlParserUtil.parse( + "CREATE SERVER s TYPE 'pg' VERSION '18' FOREIGN DATA WRAPPER w OPTIONS(host 'local')"); + List visited = new ArrayList<>(); + create.visitExpressions(visited::add); + assertEquals(3, visited.size()); + create.setVersion(new NullValue()); + create.setForeignDataWrapper("other_wrapper"); + create.getOptions().get(0).setValue(new StringValue("remote")); + assertTrue(create.toString().contains("VERSION NULL FOREIGN DATA WRAPPER other_wrapper")); + roundTrip(create); + AlterServer alter = (AlterServer) CCJSqlParserUtil.parse("ALTER SERVER s VERSION NULL"); + assertInstanceOf(NullValue.class, alter.getVersion()); + alter.setAction(ForeignObjectAlterAction.RENAME); + alter.setNewName("new_server"); + visited.clear(); + alter.visitExpressions(visited::add); + assertTrue(visited.isEmpty()); + assertEquals("ALTER SERVER s RENAME TO new_server", alter.toString()); + } + + @Test + void sharedActionsKeepObjectSpecificOptionsInactiveAndRestorable() throws JSQLParserException { + AlterServer server = (AlterServer) CCJSqlParserUtil.parse( + "ALTER SERVER s VERSION NULL OPTIONS(SET host 'remote')"); + AlterForeignDataWrapper wrapper = (AlterForeignDataWrapper) CCJSqlParserUtil.parse( + "ALTER FOREIGN DATA WRAPPER w NO HANDLER VALIDATOR public.validator " + + "OPTIONS(SET x 'y')"); + assertEquals(ForeignObjectAlterAction.OPTIONS, server.getAction()); + assertSame(server.getAction(), wrapper.getAction()); + String serverOptions = server.toString(); + String wrapperOptions = wrapper.toString(); + + server.setOwner("CURRENT_ROLE"); + wrapper.setOwner("CURRENT_USER"); + server.setAction(ForeignObjectAlterAction.OWNER); + wrapper.setAction(ForeignObjectAlterAction.OWNER); + assertEquals("ALTER SERVER s OWNER TO CURRENT_ROLE", server.toString()); + assertEquals("ALTER FOREIGN DATA WRAPPER w OWNER TO CURRENT_USER", wrapper.toString()); + roundTrip(server); + roundTrip(wrapper); + + server.setNewName("\"new server\""); + wrapper.setNewName("\"new wrapper\""); + server.setAction(ForeignObjectAlterAction.RENAME); + wrapper.setAction(ForeignObjectAlterAction.RENAME); + assertEquals("ALTER SERVER s RENAME TO \"new server\"", server.toString()); + assertEquals("ALTER FOREIGN DATA WRAPPER w RENAME TO \"new wrapper\"", wrapper.toString()); + roundTrip(server); + roundTrip(wrapper); + + List visited = new ArrayList<>(); + server.visitExpressions(visited::add); + wrapper.visitExpressions(visited::add); + assertTrue(visited.isEmpty()); + server.setAction(null); + wrapper.setAction(null); + assertNull(server.getAction()); + assertNull(wrapper.getAction()); + server.visitExpressions(visited::add); + wrapper.visitExpressions(visited::add); + assertTrue(visited.isEmpty()); + + server.setAction(ForeignObjectAlterAction.OPTIONS); + wrapper.setAction(ForeignObjectAlterAction.OPTIONS); + assertEquals(serverOptions, server.toString()); + assertEquals(wrapperOptions, wrapper.toString()); + server.visitExpressions(visited::add); + wrapper.visitExpressions(visited::add); + assertEquals(List.of(server.getOptions().get(0).getValue(), server.getVersion(), + wrapper.getOptions().get(0).getValue()), visited); + assertInstanceOf(NullValue.class, server.getVersion()); + assertTrue(wrapper.getFunctions().isHandlerSpecified()); + assertNull(wrapper.getFunctions().getHandler()); + roundTrip(server); + roundTrip(wrapper); + + server.setVersion(null); + wrapper.getFunctions().clearHandler(); + assertEquals("ALTER SERVER s OPTIONS (SET host 'remote')", server.toString()); + assertEquals("ALTER FOREIGN DATA WRAPPER w VALIDATOR public.validator OPTIONS (SET x 'y')", + wrapper.toString()); + roundTrip(server); + roundTrip(wrapper); + } + + @Test + void userMappingOptionsUseExistingForeignDataNodes() throws JSQLParserException { + CreateUserMapping create = (CreateUserMapping) CCJSqlParserUtil.parse( + "CREATE USER MAPPING FOR CURRENT_USER SERVER s OPTIONS(user 'u')"); + assertNull(create.getOptions().get(0).getAction()); + AlterUserMapping alter = (AlterUserMapping) CCJSqlParserUtil.parse( + "ALTER USER MAPPING FOR CURRENT_USER SERVER s OPTIONS(SET user 'u2')"); + assertEquals(ForeignDataOption.Action.SET, alter.getOptions().get(0).getAction()); + alter.getOptions().get(0).setAction(ForeignDataOption.Action.DROP); + assertNull(alter.getOptions().get(0).getValue()); + List expressions = new ArrayList<>(); + alter.visitExpressions(expressions::add); + assertTrue(expressions.isEmpty()); + roundTrip(alter); + assertEquals("s", create.accept(new StatementVisitorAdapter() { + @Override + public String visit(CreateUserMapping statement, S context) { + return statement.getServer(); + } + }, null)); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE OR REPLACE SERVER s FOREIGN DATA WRAPPER w", + "CREATE OR REPLACE FOREIGN DATA WRAPPER w", "CREATE SERVER s OPTIONS(x 'y')", + "CREATE FOREIGN DATA WRAPPER w HANDLER h HANDLER h2", + "CREATE FOREIGN DATA WRAPPER w NO VALIDATOR VALIDATOR v", + "ALTER FOREIGN DATA WRAPPER w", "ALTER SERVER s", + "ALTER USER MAPPING FOR USER SERVER s", + "CREATE USER MAPPING FOR USER SERVER s OPTIONS(SET x 'y')", + "CREATE SERVER s FOREIGN DATA WRAPPER w OPTIONS(DROP x)", + "ALTER SERVER s OPTIONS(SET x)", "ALTER SERVER s OPTIONS(DROP x 'y')", + "ALTER SERVER s OPTIONS()", "ALTER SERVER s OPTIONS(host = 'localhost')", + "CREATE SERVER s TYPE NULL FOREIGN DATA WRAPPER w", "ALTER SERVER s VERSION 18", + "ALTER SERVER s RENAME TO s2 OPTIONS(x 'y')", + "DROP USER MAPPING FOR u", "DROP USER MAPPING FOR u SERVER s CASCADE", + "DROP USER MAPPING IF EXISTS u SERVER s"}) + void rejectsWrongObjectAndOptionForms(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignKeyActionColumnsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignKeyActionColumnsTest.java new file mode 100644 index 0000000000..55afa3b0af --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignKeyActionColumnsTest.java @@ -0,0 +1,91 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2023 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.ReferentialAction; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.ForeignKeyReference; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlForeignKeyActionColumnsTest { + @ParameterizedTest + @ValueSource(strings = {"NULL", "DEFAULT"}) + void sharesActionsAcrossCreateAndAlter(String value) throws JSQLParserException { + for (String columns : new String[] {"author_id", "tenant_id, author_id", "\"Author Id\""}) { + for (boolean updateFirst : new boolean[] {false, true}) { + String delete = "ON DELETE SET " + value + " (" + columns + ")"; + String actions = updateFirst ? "ON UPDATE CASCADE " + delete + : delete + " ON UPDATE CASCADE"; + for (String prefix : new String[] { + "CREATE TABLE posts (tenant_id INT, author_id INT, ", + "ALTER TABLE posts ADD "}) { + String sql = prefix + "CONSTRAINT fk FOREIGN KEY (tenant_id, author_id) " + + "REFERENCES users (tenant_id, id) MATCH SIMPLE " + actions + + (prefix.startsWith("CREATE") ? ")" : ""); + Statement statement = CCJSqlParserUtil.parse(sql); + ForeignKeyConstraint index = + (ForeignKeyConstraint) (statement instanceof CreateTable + ? ((CreateTable) statement).getTableConstraints().get(0) + : ((Alter) statement).getAlterExpressions().get(0) + .getConstraint()); + ReferentialAction action = + index.getReferentialAction(ReferentialAction.Type.DELETE); + assertEquals(List.of(columns.split(", ")), action.getColumnNames()); + assertNull(index.getReferentialAction(ReferentialAction.Type.UPDATE) + .getColumnNames()); + roundTrip(statement); + action.setColumnNames(List.of("author_id")); + roundTrip(statement); + assertTrue(statement.toString().contains("SET " + value + " (author_id)")); + action.setColumnNames(null); + roundTrip(statement); + } + } + } + } + + @Test + void supportsColumnReferencesAndStatementBoundaries() throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE posts (author_id INT REFERENCES users ON DELETE SET NULL (author_id) NOT NULL)"); + ForeignKeyReference reference = table.getColumnDefinitions().get(0) + .getColumnOptions().get(0).getForeignKeyReference(); + assertEquals(List.of("author_id"), + reference.getReferentialAction(ReferentialAction.Type.DELETE).getColumnNames()); + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1").size()); + } + + @ParameterizedTest + @ValueSource(strings = {"ON UPDATE SET NULL (a)", "ON UPDATE SET DEFAULT (a)", + "ON DELETE CASCADE (a)", "ON DELETE SET NULL ()", "ON DELETE SET NULL (a,)", + "ON DELETE SET NULL (a + 1)"}) + void rejectsInvalidActionColumns(String action) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "CREATE TABLE t (a INT, FOREIGN KEY (a) REFERENCES p (a) " + action + ")")); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), CCJSqlParserUtil.parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignTableTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignTableTest.java new file mode 100644 index 0000000000..32e39be1a6 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignTableTest.java @@ -0,0 +1,155 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterForeignDataOptions; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlForeignTableTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE FOREIGN TABLE ft(id INT,label TEXT) SERVER ddl_audit_server", + "CREATE FOREIGN TABLE ft(id INT OPTIONS(column_name 'remote_id'),label TEXT) SERVER ddl_audit_server OPTIONS(schema_name 'remote',table_name 'remote_t')", + "CREATE FOREIGN TABLE ft(id INT CONSTRAINT nn NOT NULL NO INHERIT,label TEXT) SERVER ddl_audit_server", + "CREATE FOREIGN TABLE ft(id INT,label TEXT,CONSTRAINT ck CHECK(id>0)) SERVER ddl_audit_server", + "CREATE FOREIGN TABLE ft(LIKE t INCLUDING ALL) SERVER ddl_audit_server", + "CREATE FOREIGN TABLE ft PARTITION OF t FOR VALUES FROM(0) TO(100) SERVER ddl_audit_server", + "ALTER FOREIGN TABLE ft ADD COLUMN extra INT DEFAULT 1", + "ALTER FOREIGN TABLE ft DROP COLUMN IF EXISTS label", + "ALTER FOREIGN TABLE ft ALTER COLUMN id TYPE BIGINT", + "ALTER FOREIGN TABLE ft ALTER COLUMN id SET DEFAULT 2", + "ALTER FOREIGN TABLE ft ALTER COLUMN id SET NOT NULL", + "ALTER FOREIGN TABLE ft ALTER COLUMN id SET STATISTICS 200", + "ALTER FOREIGN TABLE ft ALTER COLUMN label SET STORAGE MAIN", + "ALTER FOREIGN TABLE ft ALTER COLUMN id OPTIONS(SET column_name 'new_id')", + "ALTER FOREIGN TABLE ft OPTIONS(SET table_name 'renamed', ADD schema_name 'public')", + "ALTER FOREIGN TABLE ft RENAME COLUMN id TO new_id", + "ALTER FOREIGN TABLE ft RENAME TO ft2", + "ALTER FOREIGN TABLE ft SET SCHEMA ddl_aux", + "ALTER FOREIGN TABLE ft ADD CONSTRAINT ck CHECK(id>0) NOT VALID", + "ALTER FOREIGN TABLE ft VALIDATE CONSTRAINT ck", + "DROP FOREIGN TABLE ft", + "DROP FOREIGN TABLE IF EXISTS ft CASCADE", + "ALTER FOREIGN TABLE IF EXISTS ONLY ft ALTER COLUMN id SET DEFAULT 2", + "ALTER FOREIGN TABLE ft * ALTER COLUMN id SET DEFAULT 2", + "ALTER FOREIGN TABLE ft OPTIONS(DROP table_name)", + "ALTER FOREIGN TABLE ft ALTER id OPTIONS(column_name 'remote')", + "ALTER FOREIGN TABLE ft ADD COLUMN id2 INT OPTIONS(column_name 'remote2')", + "CREATE FOREIGN TABLE ft() SERVER srv", + "DROP FOREIGN TABLE ft, public.ft2 RESTRICT"}) + void foreignDefinitionsAndActionsRoundTrip(String sql) throws JSQLParserException { + Statement statement = parse(sql); + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + if (statement instanceof CreateTable) { + assertNotNull(((CreateTable) statement).getForeignTableOptions()); + } else if (statement instanceof Alter) { + assertTrue(((Alter) statement).isForeignTable()); + } else { + assertEquals(Drop.ObjectType.FOREIGN_TABLE, ((Drop) statement).getObjectType()); + } + } + + @Test + void serverAndColumnOptionsAreMutableAndVisited() throws JSQLParserException { + CreateTable table = (CreateTable) parse( + "CREATE FOREIGN TABLE ft(id INT OPTIONS(column_name 'remote')) SERVER srv OPTIONS(table_name 'source')"); + table.getForeignTableOptions().setServer("new_srv"); + table.getForeignTableOptions().getOptions().get(0).setValue(new StringValue("target")); + table.getColumnDefinitions().get(0).getColumnOptions().get(0).getForeignOptions() + .get(0).setValue(new StringValue("remote_id")); + assertEquals( + "CREATE FOREIGN TABLE ft (id INT OPTIONS (column_name 'remote_id')) SERVER new_srv OPTIONS (table_name 'target')", + table.toString()); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(table, visited::add, ignored -> { + }); + assertEquals(2, visited.size()); + assertEquals(Set.of("ft"), new TablesNamesFinder().getTables(table)); + assertRoundTrip(table); + } + + @Test + void replacingLegacyOptionsRemovesTheOldServer() throws JSQLParserException { + CreateTable table = (CreateTable) parse( + "CREATE FOREIGN TABLE ft(id INT) SERVER old_srv OPTIONS(table_name 'old')"); + assertTrue(table.getTableOptionsStrings().contains("SERVER")); + table.setTableOptionsStrings(List.of("SERVER", "new_srv")); + assertNull(table.getForeignTableOptions()); + assertEquals("CREATE FOREIGN TABLE ft (id INT) SERVER new_srv", table.toString()); + assertRoundTrip(table); + } + + @Test + void optionActionChangesDropStaleValues() throws JSQLParserException { + Alter alter = (Alter) parse( + "ALTER FOREIGN TABLE ft ALTER COLUMN id OPTIONS(SET column_name 'old')"); + AlterForeignDataOptions action = + (AlterForeignDataOptions) alter.getAlterExpressions().get(0); + assertEquals("id", action.getColumnName()); + ForeignDataOption option = action.getOptions().get(0); + option.setAction(ForeignDataOption.Action.DROP); + assertNull(option.getValue()); + assertEquals("ALTER FOREIGN TABLE ft ALTER COLUMN id OPTIONS (DROP column_name)", + alter.toString()); + assertRoundTrip(alter); + option.setAction(ForeignDataOption.Action.ADD); + option.setValue(new StringValue("new")); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(action, visited::add, ignored -> { + }); + assertEquals(1, visited.size()); + assertRoundTrip(alter); + Drop drop = (Drop) parse("DROP FOREIGN TABLE ft, other.ft2 CASCADE"); + assertEquals(Set.of("ft", "other.ft2"), new TablesNamesFinder().getTables(drop)); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE FOREIGN TABLE ft(id INT)", + "CREATE FOREIGN TABLE ft(id INT) SERVER srv OPTIONS(ADD name 'value')", + "CREATE FOREIGN TABLE ft(id INT OPTIONS(column_name=1)) SERVER srv", + "ALTER FOREIGN TABLE ft OPTIONS(SET name)", + "ALTER FOREIGN TABLE ft OPTIONS(DROP name 'value')", + "ALTER FOREIGN TABLE ft OPTIONS()", "ALTER TABLE t OPTIONS(name 'value')", + "ALTER FOREIGN TABLE ONLY ft * ADD COLUMN extra INT"}) + void invalidForeignOptionsFail(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlPolicyDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlPolicyDdlTest.java new file mode 100644 index 0000000000..294dc55994 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlPolicyDdlTest.java @@ -0,0 +1,112 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.create.policy.CreatePolicy; +import net.sf.jsqlparser.statement.create.policy.PolicyOptions; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlPolicyDdlTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE POLICY pol ON t", + "CREATE POLICY pol ON t AS RESTRICTIVE FOR SELECT TO PUBLIC USING(id > 0)", + "CREATE POLICY pol ON t FOR INSERT TO CURRENT_USER WITH CHECK(id>0)", + "CREATE POLICY pol ON t FOR UPDATE TO CURRENT_ROLE, SESSION_USER USING(id>0) WITH CHECK(id<100)", + "ALTER POLICY pol ON t RENAME TO pol2", "ALTER POLICY pol ON t TO PUBLIC", + "ALTER POLICY pol ON t TO CURRENT_USER, SESSION_USER", + "ALTER POLICY pol ON t USING(id>1)", + "ALTER POLICY pol ON t WITH CHECK(id<10)", + "ALTER POLICY pol ON t TO CURRENT_ROLE USING(id>0) WITH CHECK(id<100)", + "DROP POLICY pol ON t", "DROP POLICY IF EXISTS pol ON t", + "DROP POLICY pol ON t CASCADE", + "DROP POLICY IF EXISTS pol ON t RESTRICT"}) + void statementsAndFollowingBoundariesRoundTrip(String sql) throws JSQLParserException { + assertRoundTrip(parse(sql)); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void createAndAlterShareRolesAndPredicates() throws JSQLParserException { + CreatePolicy create = (CreatePolicy) parse("CREATE POLICY pol ON t TO PUBLIC USING(id>0)"); + AlterPolicy alter = (AlterPolicy) parse("ALTER POLICY pol ON t TO PUBLIC USING(id>0)"); + assertEquals(create.getRoles(), alter.getOptions().getRoles()); + assertEquals(create.getUsingExpression().toString(), + alter.getOptions().getUsingExpression().toString()); + alter.getOptions().setUsingExpression(CCJSqlParserUtil.parseCondExpression("id > 10")); + alter.setPolicyName("new_policy"); + alter.setTable(new Table("new_table")); + assertEquals("ALTER POLICY new_policy ON new_table TO PUBLIC USING (id > 10)", + alter.toString()); + assertRoundTrip(alter); + alter.setNewName("renamed"); + assertEquals("ALTER POLICY new_policy ON new_table RENAME TO renamed", alter.toString()); + alter.setOptions(new PolicyOptions() + .setUsingExpression(CCJSqlParserUtil.parseCondExpression("id > 1"))); + assertNull(alter.getNewName()); + assertRoundTrip(alter); + } + + @Test + void predicatesAndTargetsParticipateInTraversal() throws JSQLParserException { + String sql = "ALTER POLICY pol ON target USING (EXISTS (SELECT 1 FROM permissions))" + + " WITH CHECK (EXISTS (SELECT 1 FROM allowed_values))"; + assertEquals(Set.of("target", "permissions", "allowed_values"), + TablesNamesFinder.findTables(sql)); + assertEquals(Set.of("target"), TablesNamesFinder.findTables("DROP POLICY pol ON target")); + assertRoundTrip(parse(sql)); + } + + @Test + void dropPolicyNamesAndBehaviorAreMutable() throws JSQLParserException { + DropPolicy drop = (DropPolicy) parse("DROP POLICY IF EXISTS pol ON t CASCADE"); + assertTrue(drop.isIfExists()); + drop.setPolicyName("new_pol").setTable(new Table("new_t")) + .setBehavior(CascadeBehavior.RESTRICT); + assertEquals("DROP POLICY IF EXISTS new_pol ON new_t RESTRICT", drop.toString()); + assertRoundTrip(drop); + } + + @Test + void createOnlyOptionsAndMalformedDropAreRejected() { + for (String sql : new String[] {"ALTER POLICY pol ON t AS RESTRICTIVE", + "ALTER POLICY pol ON t FOR SELECT", + "ALTER POLICY pol ON t RENAME TO pol2 USING(id>0)", "DROP POLICY pol", + "DROP POLICY pol ON t, u"}) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder builder = new StringBuilder(); + statement.accept(new StatementDeParser(builder), null); + assertEquals(statement.toString(), builder.toString()); + assertEquals(statement.toString(), parse(builder.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlReplicationDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlReplicationDdlTest.java new file mode 100644 index 0000000000..02e6c5566e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlReplicationDdlTest.java @@ -0,0 +1,209 @@ +/*- + * #%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.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.alter.AlterPublication; +import net.sf.jsqlparser.statement.alter.AlterSubscription; +import net.sf.jsqlparser.statement.create.publication.CreatePublication; +import net.sf.jsqlparser.statement.create.publication.PublicationOption; +import net.sf.jsqlparser.statement.create.publication.PublicationTable; +import net.sf.jsqlparser.statement.create.publication.PublicationTarget; +import net.sf.jsqlparser.statement.create.subscription.CreateSubscription; +import net.sf.jsqlparser.statement.create.subscription.SubscriptionOption; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlReplicationDdlTest { + static Stream statements() throws Exception { + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + PostgreSqlReplicationDdlTest.class + .getResourceAsStream("/postgresql/replication-ddl.sql"), + StandardCharsets.UTF_8))) { + return reader.lines() + .filter(line -> !line.trim().isEmpty() && !line.trim().startsWith("--")) + .collect(Collectors.toList()).stream(); + } + } + + @ParameterizedTest + @MethodSource("statements") + void testRoundTripAndValidation(String sql) throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql); + assertThat(statement).isInstanceOfAny(CreatePublication.class, AlterPublication.class, + CreateSubscription.class, AlterSubscription.class); + assertThat(CCJSqlParserUtil.parse(statement.toString()).toString()) + .isEqualTo(statement.toString()); + assertThat(statement.getFeatures().getCertain()).contains(StmtFeature.MODIFIES_SCHEMA) + .doesNotContain(StmtFeature.RETURNS_RESULT_SET); + validateNoErrors(sql, 1, DatabaseType.POSTGRESQL); + } + + @Test + void testPublicationTargets() throws Exception { + CreatePublication publication = (CreatePublication) CCJSqlParserUtil.parse( + "CREATE PUBLICATION mixed FOR TABLE ONLY app.users WHERE (id > 0), orders *, TABLES IN SCHEMA app, audit, TABLE events"); + assertThat(publication.isAllTables()).isFalse(); + assertThat(publication.getTargets()).extracting(PublicationTarget::getKind) + .containsExactly(PublicationTarget.Kind.TABLE, + PublicationTarget.Kind.TABLES_IN_SCHEMA, PublicationTarget.Kind.TABLE); + PublicationTable first = publication.getTargets().get(0).getTables().get(0); + assertThat(first.isOnly()).isTrue(); + assertThat(first.getWhere()).isNotNull(); + assertThat(publication.getTargets().get(0).getTables().get(1).isIncludeDescendants()) + .isTrue(); + assertThat(publication.getTargets().get(1).getSchemas()).containsExactly("app", "audit"); + assertThat(TablesNamesFinder.findTables(publication.toString())) + .containsExactlyInAnyOrder("app.users", "orders", "events"); + assertThat(TablesNamesFinder.findTables("CREATE PUBLICATION p FOR ALL TABLES")).isEmpty(); + assertThat(TablesNamesFinder.findTables("CREATE PUBLICATION p FOR TABLES IN SCHEMA app")) + .isEmpty(); + } + + @Test + void testPublicationOptions() throws Exception { + CreatePublication publication = (CreatePublication) CCJSqlParserUtil.parse( + "CREATE PUBLICATION p FOR ALL TABLES WITH (publish = 'insert, update', publish_via_partition_root, publish_generated_columns = stored)"); + assertThat(publication.getOptions().get(0).getPublishOperations()) + .containsExactlyInAnyOrder(PublicationOption.Operation.INSERT, + PublicationOption.Operation.UPDATE); + assertThat(publication.getOptions().get(1).getBooleanValue()).isTrue(); + assertThat(publication.getOptions().get(1).getValue()).isNull(); + assertThat(publication.getOptions().get(2).getGeneratedColumns()) + .isEqualTo(PublicationOption.GeneratedColumns.STORED); + assertThat( + ((CreatePublication) CCJSqlParserUtil.parse("CREATE PUBLICATION p")).getOptions()) + .isEmpty(); + publication.setName("new_publication"); + assertThat(CCJSqlParserUtil.parse(publication.toString()).toString()) + .startsWith("CREATE PUBLICATION new_publication"); + } + + @Test + void testSubscriptionOptionsAndNames() throws Exception { + CreateSubscription subscription = (CreateSubscription) CCJSqlParserUtil.parse( + "CREATE SUBSCRIPTION sub CONNECTION 'dbname=app' PUBLICATION first_pub, second_pub WITH (connect = false, streaming = parallel, slot_name = NONE)"); + assertThat(subscription.getConnection().getValue()).isEqualTo("dbname=app"); + assertThat(subscription.getPublications()).containsExactly("first_pub", "second_pub"); + assertThat(subscription.getOptions().get(0).getBooleanValue()).isFalse(); + assertThat(subscription.getOptions().get(1).getStreaming()) + .isEqualTo(SubscriptionOption.Streaming.PARALLEL); + assertThat(subscription.getOptions().get(2).isSlotNameNone()).isTrue(); + assertThat(new SubscriptionOption("slot_name", SubscriptionOption.Kind.SLOT_NAME, + new StringValue("NONE")).isSlotNameNone()).isFalse(); + assertThat(TablesNamesFinder.findTables(subscription.toString())).isEmpty(); + assertThat(subscription.getFeatures().getUncertain()) + .doesNotContain(StmtFeature.MODIFIES_DATA); + } + + @Test + void testAlterActions() throws Exception { + AlterPublication publication = + (AlterPublication) CCJSqlParserUtil.parse("ALTER PUBLICATION p DROP TABLE users"); + assertThat(publication.getAction()).isEqualTo(AlterPublication.Action.DROP); + assertThat(publication.getTargets().get(0).getTables().get(0).getColumns()).isNull(); + AlterSubscription subscription = (AlterSubscription) CCJSqlParserUtil + .parse("ALTER SUBSCRIPTION sub SET PUBLICATION p WITH (refresh = false)"); + assertThat(subscription.getAction()).isEqualTo(AlterSubscription.Action.SET_PUBLICATION); + assertThat(subscription.getOptions().get(0).getKind()) + .isEqualTo(SubscriptionOption.Kind.REFRESH); + AlterSubscription skip = (AlterSubscription) CCJSqlParserUtil + .parse("ALTER SUBSCRIPTION sub SKIP (lsn = '0/123')"); + assertThat(skip.getAction()).isEqualTo(AlterSubscription.Action.SKIP); + assertThat(skip.getOptions().get(0).getValue()).isInstanceOf(StringValue.class); + } + + @Test + void testVisitorAndCustomDeparser() throws Exception { + String sql = "CREATE PUBLICATION p FOR TABLE users (id) WHERE (id > 0)"; + Statement statement = CCJSqlParserUtil.parse(sql); + List columns = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + assertThat(context).isEqualTo("publication"); + columns.add(column.getColumnName()); + return null; + } + }; + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "publication"); + assertThat(columns).containsExactly("id", "id"); + + StringBuilder output = new StringBuilder(); + ExpressionDeParser deparser = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("new_").append(column.getColumnName()); + } + }; + statement.accept(new StatementDeParser(deparser, new SelectDeParser(), output)); + assertThat(output.toString()).contains("users (new_id) WHERE (new_id > 0)"); + assertThat(statement.toString()).doesNotContain("new_id"); + CCJSqlParserUtil.parse(output.toString()); + } + + @Test + void testProgrammaticConstruction() throws Exception { + CreateSubscription subscription = new CreateSubscription(); + subscription.setName("sub"); + subscription.setConnection(new StringValue("dbname=app")); + subscription.getPublications().add("pub"); + subscription.getOptions() + .add(new SubscriptionOption("connect", SubscriptionOption.Kind.CONNECT, + CCJSqlParserUtil.parseExpression("false"))); + assertThat(CCJSqlParserUtil.parse(subscription.toString()).toString()) + .isEqualTo(subscription.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER SUBSCRIPTION sub SKIP (lsn)", + "ALTER SUBSCRIPTION sub SKIP (copy_data = false)", "CREATE OR REPLACE PUBLICATION p", + "CREATE OR REPLACE SUBSCRIPTION sub CONNECTION 'dbname=app' PUBLICATION p", + "CREATE PUBLICATION p FOR TABLE", "CREATE PUBLICATION p FOR TABLE users ()", + "CREATE PUBLICATION p FOR TABLE users WHERE id > 0", + "CREATE PUBLICATION p FOR TABLE ONLY users *", + "ALTER PUBLICATION p DROP TABLE users (id)", + "ALTER PUBLICATION p DROP TABLE users WHERE (id > 0)", + "CREATE SUBSCRIPTION sub PUBLICATION p", + "CREATE SUBSCRIPTION sub CONNECTION 'dbname=app' PUBLICATION", + "ALTER SUBSCRIPTION sub SET ()", "CREATE PUBLICATION p WITH (unknown = true)"}) + void testInvalidSyntax(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRoutineArgumentTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRoutineArgumentTest.java new file mode 100644 index 0000000000..64dc8078f0 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRoutineArgumentTest.java @@ -0,0 +1,72 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterExtension; +import net.sf.jsqlparser.statement.grant.Grant; +import net.sf.jsqlparser.statement.RoutineReference; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlRoutineArgumentTest { + @ParameterizedTest + @ValueSource(strings = {"integer", "text[]", "IN value integer", "OUT result text", + "INOUT state app.custom_type", "VARIADIC items text[]", + "\"argument name\" double precision"}) + void privilegeAndExtensionArgumentsHaveEquivalentAst(String argument) + throws JSQLParserException { + Grant grant = (Grant) CCJSqlParserUtil.parse( + "GRANT EXECUTE ON FUNCTION app.f(" + argument + ") TO reader"); + AlterExtension extension = (AlterExtension) CCJSqlParserUtil.parse( + "ALTER EXTENSION example ADD FUNCTION app.f(" + argument + ")"); + RoutineReference.Argument access = + grant.getTarget().getRoutines().get(0).getArguments().get(0); + RoutineReference.Argument member = extension.getMember().getRoutine().getArguments().get(0); + assertEquals(access.getMode(), member.getMode()); + assertEquals(access.getName(), member.getName()); + assertEquals(access.getDataType().toString(), member.getDataType().toString()); + assertRoundTrip(grant); + assertRoundTrip(extension); + } + + @Test + void aggregateKeepsOrderByArgumentBoundary() throws JSQLParserException { + AlterExtension extension = (AlterExtension) CCJSqlParserUtil.parse( + "ALTER EXTENSION example ADD AGGREGATE app.percentile(double precision ORDER BY integer)"); + RoutineReference routine = extension.getMember().getRoutine(); + assertEquals(1, routine.getArguments().size()); + assertEquals(1, routine.getOrderByArguments().size()); + assertEquals("double precision", routine.getArguments().get(0).getDataType().toString()); + assertRoundTrip(extension); + } + + @ParameterizedTest + @ValueSource(strings = { + "GRANT EXECUTE ON FUNCTION app.f(integer ORDER BY integer) TO reader", + "GRANT EXECUTE ON FUNCTION app.f(*) TO reader", + "ALTER EXTENSION example ADD FUNCTION app.f(integer ORDER BY integer)", + "ALTER EXTENSION example ADD FUNCTION app.f(*)", + "GRANT EXECUTE ON FUNCTION app.f(IN) TO reader" + }) + void aggregateSyntaxDoesNotLeakIntoOtherRoutineContexts(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + assertEquals(statement.toString(), CCJSqlParserUtil.parse(statement.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRoutineBoundaryTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRoutineBoundaryTest.java new file mode 100644 index 0000000000..ef55acce89 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRoutineBoundaryTest.java @@ -0,0 +1,233 @@ +/*- + * #%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.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.CreateFunctionalStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.function.CreateFunction; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.procedure.CreateProcedure; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlRoutineBoundaryTest { + private static final String FOLLOWING = "SELECT 1994 AS after_routine"; + + static Stream quotedRoutines() { + List bodies = List.of( + "'BEGIN PERFORM ''end; SELECT 9;''; END;'", + "E'BEGIN\\nPERFORM 1;\\nEND;'", + "E'BEGIN PERFORM \\'a;b\\'; END;'", + "$$BEGIN PERFORM 'end;'; END;$$", + "$Body$BEGIN PERFORM $inner$semi; $$ END$inner$; END;$Body$", + "'BEGIN PERFORM 1;'\n' PERFORM 2; END;'", + "E'BEGIN\\n'\n'PERFORM 1; END;'"); + return Stream.of("FUNCTION", "PROCEDURE").flatMap(kind -> bodies.stream().map(body -> { + String returns = "FUNCTION".equals(kind) ? " RETURNS void" : ""; + return Arguments.of(kind + " " + body, + "CREATE OR REPLACE " + kind + " boundary_routine()" + returns + + " AS " + body + " LANGUAGE plpgsql", + body); + })); + } + + static Stream routineOptions() { + String body = "'BEGIN PERFORM 1; END;'"; + return Stream.of( + Arguments.of("language before body", + "CREATE FUNCTION boundary_routine() RETURNS void LANGUAGE 'plpgsql' AS " + + body, + body), + Arguments.of("options after body", + "CREATE FUNCTION boundary_routine() RETURNS void AS " + body + + " LANGUAGE plpgsql VOLATILE COST 100 SET search_path TO public", + body), + Arguments.of("comments around body", + "CREATE FUNCTION boundary_routine() RETURNS void AS /* END; */ " + body + + " -- next option\n LANGUAGE plpgsql /* ; SELECT 9; */", + body), + Arguments.of("continued body with comments", + "CREATE FUNCTION boundary_routine() RETURNS void AS 'BEGIN PERFORM 1;'" + + " -- continuation\n ' END;' LANGUAGE plpgsql", + "'BEGIN PERFORM 1;'\n' END;'"), + Arguments.of("SQL function", + "CREATE FUNCTION boundary_routine() RETURNS integer AS 'SELECT 42;' LANGUAGE sql", + "'SELECT 42;'"), + Arguments.of("internal function", + "CREATE FUNCTION boundary_routine(integer) RETURNS integer" + + " AS 'int4abs' LANGUAGE internal IMMUTABLE STRICT", + "'int4abs'"), + Arguments.of("parameter defaults", + "CREATE FUNCTION boundary_routine(x text DEFAULT $d$end; AS 'x'$d$," + + " y integer DEFAULT CASE WHEN true THEN 1 ELSE 2 END)" + + " RETURNS void AS " + body + " LANGUAGE plpgsql", + body), + Arguments.of("quoted names", + "CREATE FUNCTION public.\"end; routine\"(\"AS\" text DEFAULT 'END;')" + + " RETURNS void AS " + body + " LANGUAGE plpgsql", + body)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource({"quotedRoutines", "routineOptions"}) + void preservesQuotedRoutineAndFollowingStatement(String name, String declaration, + String body) throws Exception { + Statements statements = parse(declaration + "; " + FOLLOWING + ";"); + assertRoutineAndFollowing(statements, body); + Class expected = declaration.contains("PROCEDURE") ? CreateProcedure.class + : CreateFunction.class; + assertThat(statements.get(0)).isExactlyInstanceOf(expected); + assertRoutineAndFollowing(parse(statements.toString()), body); + StringBuilder deparsed = new StringBuilder(); + StatementDeParser deparser = new StatementDeParser(deparsed); + for (Statement statement : statements) { + statement.accept(deparser, null); + deparsed.append(";\n"); + } + assertRoutineAndFollowing(parse(deparsed.toString()), body); + } + + @ParameterizedTest + @ValueSource(strings = {"'BEGIN RETURN; END;'", "E'BEGIN\\nRETURN;\\nEND;'", + "$$BEGIN RETURN; END;$$"}) + void supportsUnambiguousBodiesWithoutDialect(String body) throws Exception { + String sql = "CREATE FUNCTION f() RETURNS void AS " + body + + " LANGUAGE plpgsql; " + FOLLOWING + ";"; + assertRoutineAndFollowing(CCJSqlParserUtil.parseStatements(sql), body); + } + + @ParameterizedTest + @ValueSource(strings = {"'BEGIN RETURN; END;'", "$tag$BEGIN RETURN; END;$tag$"}) + void acceptsBodyAtEndOfInputWithoutFinalSemicolon(String body) throws Exception { + Statements statements = parse("CREATE FUNCTION f() RETURNS void AS " + body + + " LANGUAGE plpgsql"); + assertThat(statements).hasSize(1); + assertThat(((CreateFunction) statements.get(0)).getFunctionDeclarationParts()) + .contains(body); + } + + @Test + void keepsConsecutiveFunctionsProceduresAndDdlSeparate() throws Exception { + String sql = "CREATE FUNCTION f() RETURNS integer AS 'SELECT 1;' LANGUAGE sql;" + + "CREATE PROCEDURE p() AS 'BEGIN NULL; END;' LANGUAGE plpgsql;" + + "CREATE FUNCTION g() RETURNS integer AS $g$ SELECT 2; $g$ LANGUAGE sql;" + + "CREATE TABLE after_routines(id integer); " + FOLLOWING + ";"; + Statements statements = parse(sql); + assertThat(statements).hasSize(5); + assertThat(statements.get(0)).isInstanceOf(CreateFunction.class); + assertThat(statements.get(1)).isInstanceOf(CreateProcedure.class); + assertThat(statements.get(2)).isInstanceOf(CreateFunction.class); + assertThat(statements.get(3).toString()).startsWith("CREATE TABLE after_routines"); + assertThat(statements.get(4).toString()).isEqualTo(FOLLOWING); + assertThat(parse(statements.toString())).hasSize(5); + } + + @Test + void preservesTwoLiteralExternalFunctionBody() throws Exception { + Statements statements = parse("CREATE FUNCTION f(integer) RETURNS integer" + + " AS 'library', 'symbol' LANGUAGE c; " + FOLLOWING + ";"); + assertRoutineAndFollowing(statements, "'library'"); + assertThat(((CreateFunction) statements.get(0)).getFunctionDeclarationParts()) + .containsSequence("'library'", ",", "'symbol'", "LANGUAGE", "c", ";"); + } + + @Test + void preservesReportedScriptAndIndexIssue1994() throws Exception { + // The issue deliberately omits part of the PL/pgSQL body. It stays opaque. + String sql = "DROP FUNCTION IF EXISTS \"fin\".\"restore_fund_data\"(\"in_fund_id\" int8);\n" + + "CREATE OR REPLACE FUNCTION \"fin\".\"restore_fund_data\"(\"in_fund_id\" int8)\n" + + "RETURNS \"pg_catalog\".\"varchar\" AS $BODY$\nDECLARE\nmessage VARCHAR;\n" + + "open rec_cur;\nclose proj_info_cur;\n********\nRETURN 'success';\nEND $BODY$\n" + + "LANGUAGE plpgsql VOLATILE COST 100;\n" + + "CREATE INDEX \"index_keyword\" ON \"inter\".\"inter_ti_rec\" USING btree " + + "(\"keyword\" COLLATE \"pg_catalog\".\"default\" " + + "\"pg_catalog\".\"text_ops\" ASC NULLS LAST);\n" + FOLLOWING + ";"; + Statements statements = parse(sql); + assertThat(statements).hasSize(4); + assertThat(statements.get(0)).isInstanceOf(Drop.class); + CreateFunction function = (CreateFunction) statements.get(1); + assertThat(function.getFunctionDeclarationParts()).anySatisfy(part -> assertThat(part) + .startsWith("$BODY$").endsWith("$BODY$").contains("********", "RETURN 'success';")); + CreateIndex index = (CreateIndex) statements.get(2); + assertThat(index.getIndex().getColumns().get(0).getOperatorClass()) + .isEqualTo("\"pg_catalog\".\"text_ops\""); + assertThat(statements.get(3).toString()).isEqualTo(FOLLOWING); + assertThat(parse(statements.toString())).hasSize(4); + } + + @ParameterizedTest + @ValueSource(strings = {"'BEGIN RETURN;", "$body$BEGIN RETURN; END;$other$"}) + void rejectsUnterminatedBody(String body) { + assertThrows(JSQLParserException.class, () -> parse("CREATE FUNCTION f() RETURNS void AS " + + body + " LANGUAGE plpgsql; SELECT 1994;")); + } + + @Test + void reportsInvalidStatementAfterQuotedRoutine() { + String routine = "CREATE FUNCTION f() RETURNS integer AS 'SELECT 1;' LANGUAGE sql; "; + assertThrows(JSQLParserException.class, () -> parse(routine + "SELEC 1994;")); + } + + @Test + void escapePrefixDoesNotChangeOrdinaryStringHandling() throws Exception { + String body = "e'BEGIN PERFORM \\'a;b\\'; END;'"; + Statements statements = parse("CREATE FUNCTION f() RETURNS void AS " + body + + " LANGUAGE plpgsql; SELECT 'C:\\' AS path, 'after' AS marker;"); + assertThat(statements).hasSize(2); + assertThat(((CreateFunction) statements.get(0)).getFunctionDeclarationParts()) + .contains(body); + assertThat(statements.get(1).toString()) + .isEqualTo("SELECT 'C:\\' AS path, 'after' AS marker"); + assertThat(parse(statements.toString())).hasSize(2); + } + + @Test + void retainsLegacyUnquotedRoutineBodies() throws Exception { + for (String sql : List.of("CREATE PROCEDURE p AS BEGIN NULL; END;", + "CREATE PROCEDURE p() BEGIN SELECT 1 AS 'label'; SELECT 2; END;", + "CREATE PROCEDURE p AS BEGIN SELECT $$text;$$; SELECT 2; END;", + "CREATE FUNCTION f(x integer DEFAULT CASE WHEN 1 = 1 THEN 1 ELSE 2 END) " + + "RETURN integer AS BEGIN RETURN x; END;")) { + Statements statements = CCJSqlParserUtil.parseStatements(sql + FOLLOWING + ";"); + assertThat(statements).hasSize(2); + assertThat(statements.get(1).toString()).isEqualTo(FOLLOWING); + } + } + + private static Statements parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parseStatements(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL).withUnsupportedStatements(false)); + } + + private static void assertRoutineAndFollowing(Statements statements, String body) { + assertThat(statements).hasSize(2); + assertThat(statements.get(0)).isInstanceOf(CreateFunctionalStatement.class); + CreateFunctionalStatement routine = (CreateFunctionalStatement) statements.get(0); + assertThat(routine.getFunctionDeclarationParts()).contains(body) + .doesNotContain("after_routine", "1994"); + assertThat(statements.get(1)).isInstanceOf(PlainSelect.class); + assertThat(statements.get(1).toString()).isEqualTo(FOLLOWING); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRuleTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRuleTest.java new file mode 100644 index 0000000000..e82a0e8db4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRuleTest.java @@ -0,0 +1,128 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlRuleTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE RULE r AS ON INSERT TO t DO ALSO NOTIFY ddl_probe", + "CREATE OR REPLACE RULE r AS ON UPDATE TO app.t WHERE NEW.id > 0 DO INSTEAD NOTHING", + "CREATE RULE r AS ON DELETE TO t DO NOTHING", + "CREATE RULE r AS ON INSERT TO t DO INSERT INTO log SELECT NEW.id", + "CREATE RULE r AS ON UPDATE TO t DO UPDATE log SET id=NEW.id WHERE id=OLD.id", + "CREATE RULE r AS ON DELETE TO t DO DELETE FROM log WHERE id=OLD.id", + "CREATE RULE \"_RETURN\" AS ON SELECT TO v DO INSTEAD SELECT * FROM t", + "CREATE RULE r AS ON INSERT TO t DO ALSO (NOTIFY ch, 'updated'; INSERT INTO log VALUES (1);)", + "CREATE RULE r AS ON INSERT TO t DO (;;;NOTIFY ch;;)", + "CREATE RULE r AS ON INSERT TO t DO ()", + "CREATE RULE r AS ON INSERT TO t DO (;)", + "CREATE RULE r AS ON INSERT TO t DO (SELECT 1 UNION ALL SELECT 2)", + "CREATE RULE r AS ON INSERT TO t DO (SELECT 1) UNION ALL SELECT 2", + "CREATE RULE r AS ON INSERT TO t DO TABLE log", + "CREATE RULE r AS ON INSERT TO t DO ((SELECT 1) UNION ALL SELECT 2; NOTIFY ch)", + "CREATE RULE r AS ON INSERT TO t DO WITH x AS (SELECT 1) INSERT INTO log SELECT * FROM x", + "CREATE RULE r AS ON INSERT TO t DO NOTIFY \"Channel\", 'semi;colon'", + "NOTIFY ch", "NOTIFY \"Channel\", 'it''s ready'"}) + void ruleActionsAndStatementBoundaries(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertTrue(statement instanceof CreateRule || statement instanceof NotifyStatement); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(PostgresqlVersion.V14), sql) + .validate().isEmpty()); + } + + @Test + void actionAndPredicateMutationsUseTheExistingAst() throws JSQLParserException { + CreateRule rule = (CreateRule) CCJSqlParserUtil.parse( + "CREATE RULE r AS ON INSERT TO app.t WHERE NEW.id > 0 DO (INSERT INTO log SELECT NEW.id; NOTIFY ch, 'original')"); + assertEquals(Set.of("app.t", "log"), new TablesNamesFinder().getTables(rule)); + rule.setWhereExpression(CCJSqlParserUtil.parseCondExpression("NEW.id > 10")); + Insert insert = (Insert) rule.getActions().get(0); + insert.getTable().setName("audit_log"); + NotifyStatement notify = (NotifyStatement) rule.getActions().get(1); + notify.setPayload(new StringValue("changed")); + assertTrue(rule.toString().contains("NEW.id > 10")); + assertTrue(rule.toString().contains("INSERT INTO audit_log")); + assertTrue(rule.toString().contains("'changed'")); + assertEquals(Set.of("app.t", "audit_log"), new TablesNamesFinder().getTables(rule)); + roundTrip(rule); + rule.setNothing(true); + assertTrue(rule.getActions().isEmpty()); + assertTrue(rule.toString().endsWith("DO NOTHING")); + roundTrip(rule); + } + + @Test + void nestedActionsUseCustomExpressionDeparser() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse( + "CREATE RULE r AS ON INSERT TO t DO (INSERT INTO log VALUES ('before'); NOTIFY ch, 'before')"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + return getBuilder().append("'after'"); + } + }; + SelectDeParser selects = new SelectDeParser(expressions, output); + expressions.setBuilder(output); + expressions.setSelectVisitor(selects); + statement.accept(new StatementDeParser(expressions, selects, output)); + assertFalse(output.toString().contains("'before'")); + assertTrue(output.toString().contains("VALUES ('after')")); + assertTrue(output.toString().contains("NOTIFY ch, 'after'")); + assertTrue(new TablesNamesFinder().getTables(CCJSqlParserUtil.parse("NOTIFY table_name")) + .isEmpty()); + } + + @Test + void notifyRemainsUsableAsAnIdentifier() throws JSQLParserException { + roundTrip(CCJSqlParserUtil.parse("SELECT notify FROM notify")); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE RULE r AS ON TRUNCATE TO t DO NOTHING", + "CREATE RULE r AS ON INSERT TO t DO", "CREATE RULE r AS ON INSERT TO t DO (NOTIFY ch", + "CREATE RULE r AS ON INSERT TO t DO CREATE TABLE x(id int)", + "CREATE RULE r AS ON INSERT TO t DO (NOTIFY ch NOTIFY ch2)", + "CREATE RULE r AS ON INSERT TO t DO (NOTIFY ch; DROP TABLE t)", + "CREATE RULE r AS ON INSERT TO t DO ALSO INSTEAD NOTHING", + "NOTIFY", "NOTIFY ch, 123", "NOTIFY ch, 'a' || 'b'", "NOTIFY app.ch"}) + void rejectsInvalidActionsAndPayloads(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSchemaDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSchemaDdlTest.java new file mode 100644 index 0000000000..d9044b602f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSchemaDdlTest.java @@ -0,0 +1,239 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Collections; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Sequence; +import net.sf.jsqlparser.schema.SequenceOwnership; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.LikeClause; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.IdentityAlteration; +import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; +import net.sf.jsqlparser.statement.create.sequence.CreateSequence; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; +import net.sf.jsqlparser.statement.create.table.IdentityDefinition; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * @see PostgreSQL CREATE + * TABLE + */ +class PostgreSqlSchemaDdlTest { + @Test + void testExclusionConstraint() throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE reservations (room integer, during tsrange," + + " CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =, during WITH &&)" + + " WHERE (room > 0) DEFERRABLE INITIALLY DEFERRED)"); + ExcludeConstraint constraint = (ExcludeConstraint) table.getTableConstraints().get(0); + assertThat(constraint.getName()).isEqualTo("no_overlap"); + assertThat(constraint.getUsing()).isEqualTo("gist"); + assertThat(constraint.getColumns().get(0).getExclusionOperator()).isEqualTo("="); + assertThat(constraint.getColumns().get(1).getExclusionOperator()).isEqualTo("&&"); + assertThat(constraint.getExpression()).isNotNull(); + assertThat(constraint.getConstraintAttributes().getDeferrable()).isTrue(); + assertThat(constraint.getConstraintAttributes().getInitially()) + .isEqualTo(ConstraintAttributes.Initially.DEFERRED); + } + + @Test + void testExclusionExpressionAndStorage() throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE reservations (room text, EXCLUDE USING gist ((lower(room)) WITH =)" + + " INCLUDE (room) WITH (fillfactor = 70) USING INDEX TABLESPACE archive)"); + ExcludeConstraint constraint = (ExcludeConstraint) table.getTableConstraints().get(0); + assertThat(constraint.getColumns().get(0).isExpression()).isTrue(); + assertThat(constraint.getIncludeColumns()).containsExactly("room"); + assertThat(constraint.getStorageParameters()).hasSize(1); + assertThat(constraint.getTableSpace()).isEqualTo("archive"); + } + + @Test + void testUniqueNullTreatment() throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE contacts (email text UNIQUE NULLS NOT DISTINCT," + + " CONSTRAINT contact_email UNIQUE NULLS DISTINCT (email))"); + ColumnOption option = table.getColumnDefinitions().get(0).getColumnOptions().get(0); + assertThat(option.getKind()).isEqualTo(ColumnOption.Kind.CONSTRAINT); + assertThat(((KeyConstraint) option.getConstraint()).getNullsDistinct()).isFalse(); + assertThat(((KeyConstraint) table.getTableConstraints().get(0)).getNullsDistinct()) + .isTrue(); + } + + @Test + void testPrimaryKeyInclude() throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE contacts (id integer, email text, PRIMARY KEY (id) INCLUDE (email))"); + assertThat(((KeyConstraint) table.getTableConstraints().get(0)).getIncludeColumns()) + .containsExactly("email"); + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE contacts ADD PRIMARY KEY (id) INCLUDE (email)"); + assertThat(((KeyConstraint) alter.getAlterExpressions().get(0).getConstraint()) + .getIncludeColumns()) + .containsExactly("email"); + } + + @Test + void testLikeOptionsInOrder() throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE contact_copy (LIKE contacts INCLUDING ALL EXCLUDING INDEXES," + + " extra integer, LIKE history EXCLUDING ALL INCLUDING DEFAULTS INCLUDING COMPRESSION)"); + assertThat(table.getTableElements()).hasSize(3); + LikeClause clause = table.getTableElements(LikeClause.class).get(0); + assertThat(table.getLikeTable().getName()).isEqualTo("contacts"); + assertThat(clause.getOptions()).hasSize(2); + assertThat(clause.isIncluding(LikeClause.OptionKind.DEFAULTS)).isTrue(); + assertThat(clause.isIncluding(LikeClause.OptionKind.INDEXES)).isFalse(); + assertThat(clause.isIncludingDefaults()).isNull(); + clause.addOption(LikeClause.OptionKind.INDEXES, true); + assertThat(clause.isIncluding(LikeClause.OptionKind.INDEXES)).isTrue(); + assertSqlCanBeParsedAndDeparsed(table.toString()); + } + + @Test + void testLegacyLikeSetters() { + LikeClause clause = new LikeClause(); + clause.setIncludingDefaults(true); + clause.setExcludingIdentity(true); + assertThat(clause.isIncludingDefaults()).isTrue(); + assertThat(clause.isIncludingIdentity()).isFalse(); + clause.setExcludingDefaults(null); + assertThat(clause.isIncludingDefaults()).isNull(); + } + + @Test + void testLegacyLikeTableSetterUpdatesFirstSource() throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil + .parse("CREATE TABLE copy (LIKE source INCLUDING ALL, id integer)"); + table.setLikeTable(new Table("replacement"), false); + assertThat(table.getTableElements(LikeClause.class).get(0).getTable().getName()) + .isEqualTo("replacement"); + assertThat(table.getTrailingLikeTable()).isNull(); + assertSqlCanBeParsedAndDeparsed(table.toString()); + table.setLikeTable(null, false); + assertThat(table.getLikeTable()).isNull(); + assertSqlCanBeParsedAndDeparsed(table.toString()); + } + + @Test + void testTypedTable() throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE employees OF employee_type (PRIMARY KEY (name), salary WITH OPTIONS DEFAULT 1000)"); + assertThat(table.getOfType().getDataType()).isEqualTo("employee_type"); + assertThat(table.getColumnDefinitions().get(0).isWithOptions()).isTrue(); + assertThat(table.getColumnDefinitions().get(0).getColDataType()).isNull(); + assertThat(table.getTableElements().get(0)).isInstanceOf(KeyConstraint.class); + } + + @Test + void testIdentityOptions() throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE counters (id bigint GENERATED BY DEFAULT AS IDENTITY" + + " (START WITH 100 INCREMENT BY -2 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 5), value integer)"); + IdentityDefinition identity = table.getColumnDefinitions().get(0) + .getColumnOptions().get(0).getIdentityDefinition(); + assertThat(identity.getGenerationMode()) + .isEqualTo(IdentityDefinition.GenerationMode.BY_DEFAULT); + assertThat(identity.getParameters()).hasSize(6); + assertThat(identity.getParameters().get(1).getOption()) + .isEqualTo(Sequence.ParameterType.INCREMENT_BY); + assertThat(identity.getParameters().get(1).getValue()).isEqualTo(-2L); + } + + @Test + void testAlterColumnTypeUsing() throws JSQLParserException { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE contacts ALTER COLUMN id TYPE bigint USING (id::bigint)"); + AlterExpression.ColumnDataType column = + alter.getAlterExpressions().get(0).getColDataTypeList().get(0); + assertThat(column.isWithType()).isTrue(); + assertThat(column.getUsingExpression()).isNotNull(); + column.setUsingExpression(CCJSqlParserUtil.parseExpression("id + 1")); + assertSqlCanBeParsedAndDeparsed(alter.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"ADD GENERATED ALWAYS AS IDENTITY", + "ADD GENERATED BY DEFAULT AS IDENTITY (START WITH 50)", + "SET GENERATED ALWAYS", "SET GENERATED BY DEFAULT", "SET INCREMENT BY -1", + "SET NO CYCLE", "RESTART", "RESTART WITH 200", "DROP IDENTITY IF EXISTS", + "SET GENERATED ALWAYS SET CACHE 10", "SET CACHE 10 RESTART WITH 50", + "SET NO CYCLE RESTART"}) + void testIdentityAlterations(String action) throws JSQLParserException { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE counters ALTER COLUMN id " + action); + assertThat(alter.getAlterExpressions().get(0).getColDataTypeList().get(0) + .getIdentityAlterations()) + .isNotEmpty(); + } + + @Test + void testIdentityAlterationKind() throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE counters ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY"); + IdentityAlteration action = alter.getAlterExpressions().get(0).getColDataTypeList().get(0) + .getIdentityAlterations().get(0); + assertThat(action.getKind()).isEqualTo(IdentityAlteration.Kind.ADD); + assertThat(action.getIdentityDefinition().getGenerationMode()) + .isEqualTo(IdentityDefinition.GenerationMode.ALWAYS); + } + + @Test + void testSequenceOwnership() throws JSQLParserException { + CreateSequence create = (CreateSequence) assertSqlCanBeParsedAndDeparsed( + "CREATE SEQUENCE counter_seq AS bigint INCREMENT BY -1 NO MINVALUE NO MAXVALUE NO CYCLE OWNED BY counters.id"); + assertThat(create.getSequence().getOwnership().getColumn().getColumnName()).isEqualTo("id"); + AlterSequence alter = (AlterSequence) assertSqlCanBeParsedAndDeparsed( + "ALTER SEQUENCE counter_seq OWNED BY NONE"); + assertThat(alter.getSequence().getOwnership().isNone()).isTrue(); + alter.getSequence().setOwnership(SequenceOwnership.ownedBy(new Column("app.counters.id"))); + assertSqlCanBeParsedAndDeparsed(alter.toString()); + } + + @Test + void testSequenceParametersDoNotReusePreviousValues() throws JSQLParserException { + AlterSequence alter = (AlterSequence) assertSqlCanBeParsedAndDeparsed( + "ALTER SEQUENCE counter_seq INCREMENT BY 2 INCREMENT 3 START WITH 7 START 8 RESTART"); + assertThat(alter.getSequence().getParameters().get(1).getOption()) + .isEqualTo(Sequence.ParameterType.INCREMENT); + assertThat(alter.getSequence().getParameters().get(3).getOption()) + .isEqualTo(Sequence.ParameterType.START); + assertThat(alter.getSequence().getParameters().get(4).getValue()).isNull(); + } + + @Test + void testProgrammaticIdentity() throws JSQLParserException { + CreateTable table = + (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE counters (id bigint)"); + IdentityDefinition identity = + new IdentityDefinition(IdentityDefinition.GenerationMode.ALWAYS); + identity.setParameters(Arrays.asList( + new Sequence.Parameter(Sequence.ParameterType.START_WITH).withValue(50L), + new Sequence.Parameter(Sequence.ParameterType.NO_CYCLE))); + table.getColumnDefinitions().get(0) + .setColumnOptions(Collections.singletonList(ColumnOption.identity(identity))); + assertSqlCanBeParsedAndDeparsed(table.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSchemaTraversalTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSchemaTraversalTest.java new file mode 100644 index 0000000000..09544ba84f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSchemaTraversalTest.java @@ -0,0 +1,133 @@ +/*- + * #%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.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlSchemaTraversalTest { + @Test + void testTableFinderIncludesLikeAndReferenceSources() throws JSQLParserException { + assertThat(TablesNamesFinder.findTables( + "CREATE TABLE copy (LIKE source INCLUDING ALL, parent_id integer REFERENCES parent (id))")) + .containsExactlyInAnyOrder("copy", "source", "parent"); + assertThat(TablesNamesFinder.findTables("CREATE TABLE employees OF employee_type")) + .containsExactly("employees"); + assertThat(TablesNamesFinder.findTables("ALTER SEQUENCE seq OWNED BY app.counters.id")) + .containsExactly("app.counters"); + assertThat(TablesNamesFinder.findTables("CREATE SEQUENCE seq OWNED BY NONE")).isEmpty(); + } + + @Test + void testExpressionVisitorReceivesContext() throws JSQLParserException { + List columns = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + assertThat(context).isEqualTo("context"); + columns.add(column.getColumnName()); + return null; + } + }; + StatementVisitorAdapter visitor = + new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)); + CCJSqlParserUtil.parse( + "CREATE TABLE bookings (room text, EXCLUDE USING gist ((lower(room)) WITH =) WHERE (room <> ''))") + .accept(visitor, "context"); + CCJSqlParserUtil.parse("ALTER TABLE bookings ALTER COLUMN id TYPE bigint USING id + 1") + .accept(visitor, "context"); + CCJSqlParserUtil.parse("CREATE VIEW v AS SELECT email FROM contacts") + .accept(visitor, "context"); + CCJSqlParserUtil.parse("ALTER SEQUENCE seq OWNED BY bookings.id").accept(visitor, + "context"); + assertThat(columns).containsExactly("room", "room", "id", "email", "id"); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE bookings (room text, EXCLUDE USING gist ((lower(room)) WITH =) WHERE (room <> ''))", + "ALTER TABLE bookings ALTER COLUMN room TYPE text USING lower(room)", + "CREATE VIEW v AS SELECT room FROM bookings", + "CREATE TABLE bookings (room text, CHECK (room <> ''))"}) + void testCustomExpressionDeparser(String sql) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("new_").append(column.getColumnName()); + } + }; + Statement statement = CCJSqlParserUtil.parse(sql); + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output)); + assertThat(output.toString()).contains("new_room"); + assertThat(statement.toString()).doesNotContain("new_room"); + assertThat(CCJSqlParserUtil.parse(output.toString()).getClass()) + .isEqualTo(statement.getClass()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE VIEW v AS SELECT * FROM source WITH NO DATA", + "CREATE MATERIALIZED VIEW v AS SELECT * FROM source WITH CHECK OPTION", + "CREATE RECURSIVE VIEW v AS SELECT 1", + "CREATE RECURSIVE VIEW v(n) AS SELECT 1 WITH CHECK OPTION", + "CREATE TABLE copy (LIKE source INCLUDING UNKNOWN)", + "CREATE TABLE t (id integer, UNIQUE (id) INITIALLY UNKNOWN)"}) + void testInvalidClauseCombinations(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } + + @Test + void testMaterializedViewReadEffects() throws JSQLParserException { + Statement populated = + CCJSqlParserUtil.parse("CREATE MATERIALIZED VIEW v AS SELECT * FROM source"); + assertThat(populated.getFeatures().getCertain()) + .contains(StmtFeature.MODIFIES_SCHEMA, StmtFeature.READS_DATA) + .doesNotContain(StmtFeature.RETURNS_RESULT_SET); + Statement empty = CCJSqlParserUtil + .parse("CREATE MATERIALIZED VIEW v AS SELECT * FROM source WITH NO DATA"); + assertThat(empty.getFeatures().getCertain()).contains(StmtFeature.MODIFIES_SCHEMA) + .doesNotContain(StmtFeature.READS_DATA); + Statement view = CCJSqlParserUtil.parse("CREATE VIEW v AS SELECT * FROM source"); + assertThat(view.getFeatures().getCertain()).doesNotContain(StmtFeature.READS_DATA); + } + + @Test + void testPostgreSqlValidation() throws JSQLParserException { + validateNoErrors("CREATE MATERIALIZED VIEW v AS SELECT * FROM source WITH NO DATA", 1, + DatabaseType.POSTGRESQL); + validateNoErrors( + "CREATE TABLE bookings (room integer, EXCLUDE USING gist ((room + 1) WITH =) WHERE (room > 0))", + 1, DatabaseType.POSTGRESQL); + validateNoErrors("ALTER TABLE bookings ALTER COLUMN room TYPE bigint USING room + 1", 1, + DatabaseType.POSTGRESQL); + validateNoErrors("CREATE SEQUENCE seq OWNED BY bookings.room", 1, DatabaseType.POSTGRESQL); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSequenceDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSequenceDdlTest.java new file mode 100644 index 0000000000..811ba5a67f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSequenceDdlTest.java @@ -0,0 +1,124 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; +import net.sf.jsqlparser.statement.create.sequence.CreateSequence; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlSequenceDdlTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE SEQUENCE IF NOT EXISTS s AS INTEGER START WITH 3", + "CREATE TEMP SEQUENCE s AS INTEGER START WITH 3", + "CREATE TEMP SEQUENCE IF NOT EXISTS s AS INTEGER START WITH 3", + "CREATE TEMPORARY SEQUENCE s AS INTEGER START WITH 3", + "CREATE TEMPORARY SEQUENCE IF NOT EXISTS s AS INTEGER START WITH 3", + "CREATE UNLOGGED SEQUENCE s AS INTEGER START WITH 3", + "CREATE UNLOGGED SEQUENCE IF NOT EXISTS s AS INTEGER START WITH 3", + "CREATE SEQUENCE s OWNED BY t.id CACHE 5", + "ALTER SEQUENCE s AS SMALLINT", + "ALTER SEQUENCE s OWNED BY t.id INCREMENT BY 3", + "ALTER SEQUENCE s SET UNLOGGED", + "ALTER SEQUENCE s SET LOGGED", + "ALTER SEQUENCE s OWNER TO CURRENT_USER", + "ALTER SEQUENCE s RENAME TO s2", + "ALTER SEQUENCE s SET SCHEMA ddl_aux", + "ALTER SEQUENCE IF EXISTS s AS SMALLINT", + "ALTER SEQUENCE IF EXISTS s RESTART", + "ALTER SEQUENCE IF EXISTS s RESTART WITH 7", + "ALTER SEQUENCE IF EXISTS s INCREMENT BY 2", + "ALTER SEQUENCE IF EXISTS s MINVALUE -5 MAXVALUE 100", + "ALTER SEQUENCE IF EXISTS s CACHE 3 NO CYCLE", + "ALTER SEQUENCE IF EXISTS s OWNED BY t.id", + "ALTER SEQUENCE IF EXISTS s OWNED BY t.id INCREMENT BY 3", + "ALTER SEQUENCE IF EXISTS s SET UNLOGGED", + "ALTER SEQUENCE IF EXISTS s SET LOGGED", + "ALTER SEQUENCE IF EXISTS s OWNER TO CURRENT_USER", + "ALTER SEQUENCE IF EXISTS s RENAME TO s2", + "ALTER SEQUENCE IF EXISTS s SET SCHEMA ddl_aux", + "CREATE SEQUENCE IF NOT EXISTS sequence_test", + "CREATE TEMP SEQUENCE myseq2", + "CREATE TEMP SEQUENCE myseq3", + "CREATE UNLOGGED SEQUENCE sequence_test_unlogged", + "CREATE TEMPORARY SEQUENCE sequence_test_temp1"}) + void postgresFormsRoundTrip(String sql) throws JSQLParserException { + Statement statement = parse(sql); + assertTrue(statement instanceof CreateSequence || statement instanceof AlterSequence); + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void createOptionsCanBeModifiedWithoutStaleTokens() throws JSQLParserException { + CreateSequence statement = (CreateSequence) parse( + "CREATE TEMP SEQUENCE IF NOT EXISTS s OWNED BY t.id CACHE 5 AS INTEGER"); + assertTrue(statement.isIfNotExists()); + assertEquals(CreateSequence.Persistence.TEMP, statement.getPersistence()); + assertEquals("t.id", statement.getSequence().getOwnership().getColumn().toString()); + assertEquals("INTEGER", statement.getSequence().getDataType()); + statement.setPersistence(CreateSequence.Persistence.UNLOGGED); + statement.setIfNotExists(false); + statement.getSequence().setName("renamed"); + statement.getSequence().setOwnership(null); + statement.getSequence().getParameters().get(0).setValue(10L); + assertEquals("CREATE UNLOGGED SEQUENCE renamed AS INTEGER CACHE 10", statement.toString()); + assertRoundTrip(statement); + } + + @Test + void alterOptionsAndActionsRemainSeparate() throws JSQLParserException { + AlterSequence statement = (AlterSequence) parse( + "ALTER SEQUENCE IF EXISTS s OWNED BY t.id INCREMENT BY 3 AS SMALLINT"); + assertTrue(statement.isIfExists()); + assertEquals(AlterSequence.Action.PARAMETERS, statement.getAction()); + assertEquals("SMALLINT", statement.getSequence().getDataType()); + assertEquals(3L, statement.getSequence().getParameters().get(0).getValue()); + statement.setAction(AlterSequence.Action.RENAME); + statement.setNewName("new_s"); + assertEquals("ALTER SEQUENCE IF EXISTS s RENAME TO new_s", statement.toString()); + assertRoundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"LOCAL TEMP", "LOCAL TEMPORARY", "GLOBAL TEMP", "GLOBAL TEMPORARY"}) + void scopedTemporarySequence(String prefix) throws JSQLParserException { + String sql = "CREATE " + prefix + " SEQUENCE s"; + assertEquals(sql, parse(sql).toString()); + } + + @Test + void duplicateScalarOptionsFail() { + assertThrows(JSQLParserException.class, + () -> parse("CREATE SEQUENCE s AS INT AS BIGINT")); + assertThrows(JSQLParserException.class, + () -> parse("ALTER SEQUENCE s OWNED BY NONE OWNED BY t.id")); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlStatisticsDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlStatisticsDdlTest.java new file mode 100644 index 0000000000..290b4239ce --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlStatisticsDdlTest.java @@ -0,0 +1,105 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterStatistics; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlStatisticsDdlTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE STATISTICS st ON id,label FROM t", + "CREATE STATISTICS st ON (lower(label)) FROM t", + "CREATE STATISTICS st ON (id+1),label FROM t", + "CREATE STATISTICS IF NOT EXISTS st ON id,label FROM t", + "CREATE STATISTICS IF NOT EXISTS st ON (lower(label)) FROM t", + "CREATE STATISTICS IF NOT EXISTS st ON (id+1),label FROM t", + "CREATE STATISTICS ON id,label FROM t", "CREATE STATISTICS ON (lower(label)) FROM t", + "CREATE STATISTICS ON (id+1),label FROM t", + "CREATE STATISTICS st(ndistinct) ON id,label FROM t", + "CREATE STATISTICS st(dependencies) ON id,label FROM t", + "CREATE STATISTICS st(mcv) ON id,label FROM t", + "CREATE STATISTICS st(ndistinct,dependencies,mcv) ON id,label FROM t", + "ALTER STATISTICS st OWNER TO CURRENT_USER", "ALTER STATISTICS st RENAME TO st2", + "ALTER STATISTICS st SET SCHEMA ddl_aux", "ALTER STATISTICS st SET STATISTICS 500", + "ALTER STATISTICS st SET STATISTICS DEFAULT", "DROP STATISTICS st", + "DROP STATISTICS IF EXISTS st,st2 RESTRICT"}) + void definitionAndActionsRoundTrip(String sql) throws JSQLParserException { + assertRoundTrip(parse(sql)); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void namesKindsAndKeyExpressionsCanBeChanged() throws JSQLParserException { + CreateStatistics statistics = (CreateStatistics) parse( + "CREATE STATISTICS IF NOT EXISTS old_stats(ndistinct) ON id,label FROM t"); + assertTrue(statistics.isIfNotExists()); + assertEquals(List.of(CreateStatistics.Kind.NDISTINCT), statistics.getKinds()); + statistics.setName("audit.new_stats").setKinds(List.of(CreateStatistics.Kind.DEPENDENCIES)) + .setTable(new Table("other_table")); + statistics.getExpressions().set(0, new Column("new_id")); + assertEquals( + "CREATE STATISTICS IF NOT EXISTS audit.new_stats (dependencies) ON new_id, label FROM other_table", + statistics.toString()); + assertRoundTrip(statistics); + assertEquals(Set.of("other_table"), TablesNamesFinder.findTables(statistics.toString())); + } + + @Test + void statisticsTargetDistinguishesDefault() throws JSQLParserException { + AlterStatistics statistics = + (AlterStatistics) parse("ALTER STATISTICS st SET STATISTICS DEFAULT"); + assertEquals(AlterStatistics.Action.SET_STATISTICS, statistics.getAction()); + assertNull(statistics.getStatistics()); + statistics.setName("new_st").setStatistics(200); + assertEquals("ALTER STATISTICS new_st SET STATISTICS 200", statistics.toString()); + assertRoundTrip(statistics); + assertTrue(TablesNamesFinder.findTables(statistics.toString()).isEmpty()); + } + + @Test + void malformedDefinitionsAndUnknownKindsFail() { + for (String sql : new String[] {"CREATE STATISTICS st ON id FROM t", + "CREATE STATISTICS IF NOT EXISTS ON id,label FROM t", + "CREATE STATISTICS st(unknown) ON id,label FROM t", + "CREATE STATISTICS st(mcv,mcv) ON id,label FROM t", + "CREATE STATISTICS st(mcv) ON (id+1) FROM t", + "CREATE STATISTICS st ON id+1,label FROM t", + "ALTER STATISTICS st SET STATISTICS 10001"}) { + assertThrows(JSQLParserException.class, () -> parse(sql), sql); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableColumnOverridesTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableColumnOverridesTest.java new file mode 100644 index 0000000000..0bebf9b354 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableColumnOverridesTest.java @@ -0,0 +1,99 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlTableColumnOverridesTest { + @ParameterizedTest + @ValueSource(strings = {"row_type", "public.row_type", "\"Type.Schema\".\"Row.Type\""}) + void keepsTypeNameSeparateFromFirstColumn(String type) throws JSQLParserException { + for (String withOptions : new String[] {"", " WITH OPTIONS"}) { + CreateTable table = parse("CREATE TABLE t OF " + type + " (a" + withOptions + + " DEFAULT 7 NOT NULL, b, CHECK (a > 0))"); + assertEquals(type, table.getOfType().getDataType()); + assertNull(table.getOfType().getArgumentsStringList()); + assertEquals(2, table.getColumnDefinitions().size()); + ColumnDefinition first = table.getColumnDefinitions().get(0); + assertNull(first.getColDataType()); + assertEquals(!withOptions.isEmpty(), first.isWithOptions()); + assertEquals("b", table.getColumnDefinitions().get(1).getColumnName()); + assertEquals(ColumnOption.Kind.DEFAULT, first.getColumnOptions().get(0).getKind()); + roundTrip(table); + first.getColumnOptions().get(0).setDefaultExpression(new LongValue(9)); + assertTrue(table.toString().contains("DEFAULT 9 NOT NULL")); + roundTrip(table); + } + } + + @ParameterizedTest + @ValueSource(strings = {"DEFAULT", "FOR VALUES IN (1, 2)", + "FOR VALUES FROM (0) TO (10)", "FOR VALUES WITH (MODULUS 4, REMAINDER 0)"}) + void sharesOverridesForPartitionChildren(String bound) throws JSQLParserException { + for (String options : new String[] {"", " WITH OPTIONS"}) { + CreateTable table = parse("CREATE TABLE child PARTITION OF public.parent (a" + options + + " DEFAULT 7 NOT NULL, b, CHECK (a > 0)) " + bound); + assertNotNull(table.getPartitionBound()); + assertNull(table.getColumnDefinitions().get(0).getColDataType()); + assertNull(table.getColumns()); + roundTrip(table); + List values = new ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + values.add(value.getValue()); + return getBuilder().append(value.getValue()); + } + }; + StringBuilder out = new StringBuilder(); + table.accept(new StatementDeParser(expressions, new SelectDeParser(), out)); + assertEquals(table.toString(), out.toString()); + assertEquals(1, values.stream().filter(v -> v == 7).count()); + } + } + + @Test + void singleBareOverrideAndOrdinaryTypeParametersRemainDistinct() throws JSQLParserException { + assertNotNull(parse("CREATE TABLE t OF row_type (a)").getColumnDefinitions()); + assertNotNull( + parse("CREATE TABLE child PARTITION OF parent (a) DEFAULT").getColumnDefinitions()); + assertEquals("numeric (10, 2)", parse("CREATE TABLE t (a numeric(10, 2))") + .getColumnDefinitions().get(0).getColDataType().toString()); + assertEquals(List.of("a"), parse("CREATE TABLE t (a) AS SELECT 1").getColumns()); + assertEquals(2, CCJSqlParserUtil + .parseStatements("CREATE TABLE t OF row_type (a DEFAULT 1); SELECT 1").size()); + } + + private static CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(CreateTable table) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + table.accept(new StatementDeParser(out)); + assertEquals(table.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableInheritanceTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableInheritanceTest.java new file mode 100644 index 0000000000..d6d697b737 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableInheritanceTest.java @@ -0,0 +1,78 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlTableInheritanceTest { + @ParameterizedTest + @ValueSource(strings = {"parent", "public.parent", "public.parent, other.parent2", + "\"Odd.Schema\".\"Parent.Name\""}) + void modelsAndVisitsParentTables(String parents) throws JSQLParserException { + for (String definition : new String[] {"()", "(extra INT)", "(LIKE source, extra INT)"}) { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE child " + definition + " INHERITS (" + parents + + ") WITH (fillfactor = 80)"); + List
inherited = table.getInherits(); + assertEquals(parents.split(", ").length, inherited.size()); + assertEquals(parents.split(", ")[0], inherited.get(0).getFullyQualifiedName()); + List
visited = new ArrayList<>(); + TableDefinitionTraversal.visit(table, e -> { + }, visited::add); + for (Table parent : inherited) { + assertTrue(visited.stream().anyMatch(t -> t == parent)); + assertTrue(new TablesNamesFinder().getTables(table) + .contains(parent.getFullyQualifiedName())); + } + roundTrip(table); + inherited.set(0, new Table("replacement")); + assertTrue(table.toString().contains("INHERITS (replacement")); + roundTrip(table); + } + } + + @Test + void preservesEmptyDefinitionsAndCanRemoveInheritance() throws JSQLParserException { + CreateTable table = + (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE child () INHERITS (parent)"); + assertNotNull(table.getTableElements()); + assertTrue(table.getTableElements().isEmpty()); + table.setInherits(null); + assertEquals("CREATE TABLE child ()", table.toString()); + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1").size()); + } + + @ParameterizedTest + @ValueSource(strings = {"()", "(parent,)", "(parent AS p)", "(parent + 1)"}) + void rejectsMalformedParentLists(String parents) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("CREATE TABLE child () INHERITS " + parents)); + } + + private static void roundTrip(CreateTable table) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + table.accept(new StatementDeParser(out)); + assertEquals(table.toString(), out.toString()); + assertEquals(out.toString(), CCJSqlParserUtil.parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableOptionsTest.java new file mode 100644 index 0000000000..22376ee52b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableOptionsTest.java @@ -0,0 +1,219 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.statement.create.table.TableOption.Kind; +import net.sf.jsqlparser.statement.create.table.TableOption.OnCommitAction; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlTableOptionsTest { + private static final Dialect[] DIALECTS = {Dialect.POSTGRESQL, null}; + + @ParameterizedTest + @CsvSource(value = {"PRESERVE_ROWS|ON COMMIT PRESERVE ROWS", + "DELETE_ROWS|ON COMMIT DELETE ROWS", "DROP|ON COMMIT DROP"}, delimiter = '|') + void onCommitIsOneTypedOption(OnCommitAction action, String clause) + throws JSQLParserException { + String sql = "CREATE TEMPORARY TABLE t (a INT) " + clause; + for (Dialect dialect : DIALECTS) { + CreateTable table = parse(sql, dialect); + assertEquals(1, table.getTableOptions().size()); + TableOption option = table.getTableOption(Kind.ON_COMMIT).orElseThrow(); + assertEquals(action, option.getOnCommitAction()); + assertEquals("ON COMMIT", option.getName()); + assertEquals(action.toString(), option.getValue()); + assertEquals(Arrays.asList(clause.split(" ")), option.getTokens()); + assertEquals(Arrays.asList(clause.split(" ")), table.getTableOptionsStrings()); + assertRoundTrip(table, sql, dialect); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (a INT) USING heap", + "CREATE TABLE t USING heap AS SELECT 1 AS a", + "CREATE TABLE t (a) USING heap WITH (fillfactor = 70) TABLESPACE ts AS SELECT 1 WITH NO DATA"}) + void accessMethodIsOneTypedOption(String sql) throws JSQLParserException { + for (Dialect dialect : DIALECTS) { + CreateTable table = parse(sql, dialect); + TableOption option = table.getTableOptions().get(0); + assertEquals(Kind.ACCESS_METHOD, option.getKind()); + assertEquals("USING", option.getName()); + assertEquals("heap", option.getValue()); + assertEquals(List.of("USING", "heap"), option.getTokens()); + assertEquals(List.of("USING", "heap"), table.getTableOptionsStrings().subList(0, 2)); + assertEquals(sql.contains(" AS "), table.getSelect() != null); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(table)); + assertRoundTrip(table, sql, dialect); + } + } + + @Test + void postgreSqlClausesKeepTheirOrderAndLegacyTokens() throws JSQLParserException { + String sql = "CREATE TEMP TABLE t (a INT) USING heap WITH (fillfactor = 70) " + + "ON COMMIT DELETE ROWS TABLESPACE ts"; + for (Dialect dialect : DIALECTS) { + CreateTable table = parse(sql, dialect); + assertEquals( + List.of(Kind.ACCESS_METHOD, Kind.STORAGE_PARAMETERS, Kind.ON_COMMIT, + Kind.TABLESPACE), + table.getTableOptions().stream().map(TableOption::getKind) + .collect(Collectors.toList())); + assertEquals(List.of("USING", "heap", "WITH", "(fillfactor = 70)", "ON", "COMMIT", + "DELETE", "ROWS", "TABLESPACE", "ts"), table.getTableOptionsStrings()); + assertRoundTrip(table, sql, dialect); + } + + String query = "CREATE TEMP TABLE t (a) USING heap ON COMMIT DROP AS SELECT 1 WITH NO DATA"; + for (Dialect dialect : DIALECTS) { + CreateTable table = parse(query, dialect); + assertEquals(List.of(Kind.ACCESS_METHOD, Kind.ON_COMMIT), + table.getTableOptions().stream().map(TableOption::getKind) + .collect(Collectors.toList())); + assertEquals(Boolean.FALSE, table.getWithData()); + assertRoundTrip(table, query, dialect); + } + } + + @Test + void accessMethodFollowsPostgreSqlPartitionKey() throws JSQLParserException { + String sql = "CREATE TABLE t (a INT) PARTITION BY RANGE (a) USING heap TABLESPACE ts"; + CreateTable table = parse(sql, Dialect.POSTGRESQL); + assertTrue(table.isTableOptionsAfterPartition()); + assertEquals("heap", + table.getTableOption(Kind.ACCESS_METHOD).orElseThrow().getValue()); + assertRoundTrip(table, sql, Dialect.POSTGRESQL); + } + + @ParameterizedTest + @CsvSource(value = {"PRESERVE_DEFINITION|ON COMMIT PRESERVE DEFINITION", + "DROP_DEFINITION|ON COMMIT DROP DEFINITION"}, delimiter = '|') + void oraclePrivateTemporaryTablesUseTheSameOption(OnCommitAction action, String clause) + throws JSQLParserException { + String sql = "CREATE PRIVATE TEMPORARY TABLE ora$ptt_t (a INT) " + clause; + for (Dialect dialect : new Dialect[] {Dialect.ORACLE, null}) { + CreateTable table = parse(sql, dialect); + assertEquals(1, table.getTableOptions().size()); + assertEquals(action, table.getTableOptions().get(0).getOnCommitAction()); + assertEquals(Arrays.asList(clause.split(" ")), table.getTableOptionsStrings()); + assertRoundTrip(table, sql, dialect); + } + } + + @Test + void usingIndexTablespaceRemainsRaw() throws JSQLParserException { + String sql = "CREATE TABLE t (a INT) USING INDEX TABLESPACE ts"; + CreateTable table = parse(sql, null); + assertEquals(Kind.OTHER, table.getTableOptions().get(0).getKind()); + assertEquals(List.of("USING INDEX", "TABLESPACE ts"), table.getTableOptionsStrings()); + assertRoundTrip(table, sql, null); + } + + @ParameterizedTest + @ValueSource(strings = {"ON COMMIT", "ON COMMIT DELETE", "ON COMMIT PRESERVE", + "ON COMMIT ROWS", "ON COMMIT DELETE DEFINITION"}) + void rejectsIncompleteOnCommitActions(String clause) { + for (Dialect dialect : DIALECTS) { + assertThrows(JSQLParserException.class, + () -> parse("CREATE TEMPORARY TABLE t (a INT) " + clause, dialect)); + } + } + + @Test + void editsReplaceTheOptionContents() throws JSQLParserException { + CreateTable table = parse("CREATE TEMP TABLE t (a INT) USING heap ON COMMIT DROP", + Dialect.POSTGRESQL); + TableOption accessMethod = table.getTableOption(Kind.ACCESS_METHOD).orElseThrow(); + TableOption onCommit = table.getTableOption(Kind.ON_COMMIT).orElseThrow(); + + accessMethod.setValue("columnar"); + onCommit.setOnCommitAction(OnCommitAction.PRESERVE_ROWS); + assertEquals(List.of("USING", "columnar", "ON", "COMMIT", "PRESERVE", "ROWS"), + table.getTableOptionsStrings()); + assertThrows(UnsupportedOperationException.class, () -> onCommit.getTokens().add("x")); + assertRoundTrip(table, + "CREATE TEMP TABLE t (a INT) USING columnar ON COMMIT PRESERVE ROWS", + Dialect.POSTGRESQL); + + // Raw tokens discard the action; a structured setter never restores it. + onCommit.setTokens(new ArrayList<>(List.of("ON", "COMMIT", "DROP"))); + assertEquals(Kind.OTHER, onCommit.getKind()); + assertNull(onCommit.getOnCommitAction()); + assertTrue(table.getTableOption(Kind.ON_COMMIT).isEmpty()); + onCommit.setOnCommitAction(OnCommitAction.DELETE_ROWS); + assertEquals(Kind.ON_COMMIT, onCommit.getKind()); + assertEquals("ON COMMIT DELETE ROWS", onCommit.toString()); + onCommit.setKind(Kind.COMMENT); + assertNull(onCommit.getOnCommitAction()); + onCommit.setOnCommitAction(OnCommitAction.DROP); + onCommit.setValue("DROP"); + assertNull(onCommit.getOnCommitAction()); + assertEquals("ON COMMIT DROP", onCommit.toString()); + + onCommit.setOnCommitAction(null); + assertEquals(Kind.ON_COMMIT, onCommit.getKind()); + assertNull(onCommit.getValue()); + assertEquals(List.of("ON", "COMMIT"), onCommit.getTokens()); + assertEquals("ON COMMIT", onCommit.toString()); + } + + @Test + void factoriesBuildOptionsWithoutParsing() throws JSQLParserException { + CreateTable table = new CreateTable().withTable(new Table("t")) + .withColumnDefinitions(List.of(new ColumnDefinition("a", new ColDataType("INT")))) + .withTableOptions(new ArrayList<>(List.of(TableOption.accessMethod("heap"), + TableOption.onCommit(OnCommitAction.DROP)))); + table.setCreateOptionsStrings(List.of("TEMPORARY")); + assertEquals(Kind.ON_COMMIT, table.getTableOptions().get(1).getKind()); + assertRoundTrip(table, "CREATE TEMPORARY TABLE t (a INT) USING heap ON COMMIT DROP", + Dialect.POSTGRESQL); + } + + private static CreateTable parse(String sql, Dialect dialect) throws JSQLParserException { + return (CreateTable) (dialect == null + ? CCJSqlParserUtil.parse(sql) + : CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect))); + } + + private static void assertRoundTrip(CreateTable table, String expected, Dialect dialect) + throws JSQLParserException { + assertEquals(expected, table.toString()); + StringBuilder deparsed = new StringBuilder(); + table.accept(new StatementDeParser(deparsed), null); + assertEquals(expected, deparsed.toString()); + CreateTable reparsed = parse(deparsed.toString(), dialect); + assertEquals(expected, reparsed.toString()); + assertEquals(table.getTableOptionsStrings(), reparsed.getTableOptionsStrings()); + assertEquals(2, (dialect == null + ? CCJSqlParserUtil.parseStatements(expected + "; SELECT 1") + : CCJSqlParserUtil.parseStatements(expected + "; SELECT 1", + parser -> parser.withDialect(dialect))) + .size()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableQueryOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableQueryOptionsTest.java new file mode 100644 index 0000000000..0ab8093a6f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableQueryOptionsTest.java @@ -0,0 +1,125 @@ +/*- + * #%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.statement.create; + +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.KeyElement; +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlTableQueryOptionsTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE LOCAL TEMP TABLE t (id INT)", + "CREATE LOCAL TEMPORARY TABLE t(a INT) ON COMMIT DROP", + "CREATE LOCAL TEMP TABLE t AS SELECT 1 AS id WITH NO DATA", + "CREATE LOCAL TEMPORARY TABLE t (id INT)", + "CREATE LOCAL TEMPORARY TABLE t AS SELECT 1 AS id WITH NO DATA", + "CREATE TABLE t (id INT, s TEXT) WITH (toast.autovacuum_enabled=false)", + "CREATE TABLE t(a TEXT) WITH(toast.autovacuum_enabled=false,fillfactor=80)", + "CREATE TABLE t (id INT, s TEXT) WITHOUT OIDS", + "CREATE TABLE t(a INT) WITHOUT OIDS", + "CREATE TABLE t (id INT, s TEXT) PARTITION BY RANGE(id int4_ops)", + "CREATE TABLE t(a INT,label TEXT) PARTITION BY RANGE(a int4_ops)", + "CREATE TABLE t(a INT,label TEXT) PARTITION BY RANGE(label COLLATE \"C\" text_ops)", + "CREATE TABLE t AS EXECUTE prepared_source(1)", + "CREATE TABLE t AS EXECUTE q(1) WITH NO DATA"}) + void auditedFormsRoundTrip(String sql) throws JSQLParserException { + CreateTable table = parse(sql); + assertRoundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void storageParametersStayMutableAndVisibleToVisitors() throws JSQLParserException { + CreateTable table = parse("CREATE TABLE t (id INT) WITH (toast.autovacuum_enabled=false)"); + TableOption option = + table.getTableOption(TableOption.Kind.STORAGE_PARAMETERS).orElseThrow(); + IndexOption parameter = option.getStorageParameters().get(0); + assertEquals("toast.autovacuum_enabled", parameter.getName()); + parameter.setName("fillfactor"); + parameter.setValue(new LongValue(70)); + assertEquals("CREATE TABLE t (id INT) WITH (fillfactor = 70)", table.toString()); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(table, visited::add, ignored -> { + }); + assertEquals(List.of(parameter.getValue()), visited); + assertRoundTrip(table); + } + + @Test + void partitionAttributesShareIndexKeyNodes() throws JSQLParserException { + CreateTable table = parse( + "CREATE TABLE t (id INT, other_id INT) PARTITION BY RANGE (id pg_catalog.int4_ops)"); + KeyElement key = table.getPartitioning().getKeyColumns().get(0); + assertEquals("pg_catalog.int4_ops", key.getOperatorClass()); + table.getPartitioning().getKeyColumns().set(0, + new KeyElement("other_id").withOperatorClass("int4_ops")); + assertTrue(table.toString().endsWith("PARTITION BY RANGE (other_id int4_ops)")); + assertRoundTrip(table); + } + + @Test + void partitionPrecedesTableOptions() throws JSQLParserException { + CreateTable table = + parse("CREATE TABLE t (id INT) PARTITION BY RANGE (id int4_ops) WITHOUT OIDS"); + assertTrue(table.isTableOptionsAfterPartition()); + assertEquals("CREATE TABLE t (id INT) PARTITION BY RANGE (id int4_ops) WITHOUT OIDS", + table.toString()); + assertRoundTrip(table); + } + + @Test + void executeSourceAndArgumentsRemainStructured() throws JSQLParserException { + CreateTable table = parse("CREATE TABLE t AS EXECUTE q(1) WITH NO DATA"); + assertNull(table.getSelect()); + assertEquals("q", table.getExecute().getName()); + table.getExecute().setName("q2"); + table.getExecute().getExprList().set(0, new LongValue(2)); + assertEquals("CREATE TABLE t AS EXECUTE q2 (2) WITH NO DATA", table.toString()); + assertRoundTrip(table); + table.setSelect( + (net.sf.jsqlparser.statement.select.Select) CCJSqlParserUtil.parse("SELECT 1"), + false); + assertNull(table.getExecute()); + } + + @Test + void indexOnlyOrderingIsRejectedForPartitionKeys() { + assertThrows(JSQLParserException.class, + () -> parse("CREATE TABLE t (id INT) PARTITION BY RANGE (id DESC)")); + assertThrows(JSQLParserException.class, + () -> parse("CREATE TABLE t (id INT) PARTITION BY RANGE (id NULLS FIRST)")); + } + + private static CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(CreateTable table) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + table.accept(new StatementDeParser(sql), null); + assertEquals(table.toString(), sql.toString()); + assertEquals(table.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTemporalConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTemporalConstraintTest.java new file mode 100644 index 0000000000..9842a0d603 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTemporalConstraintTest.java @@ -0,0 +1,173 @@ +/*- + * #%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.statement.create; + +import net.sf.jsqlparser.statement.create.table.KeyElement; +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.KeyColumnSource; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlTemporalConstraintTest { + @ParameterizedTest + @ValueSource(strings = {"PRIMARY KEY", "UNIQUE", "UNIQUE NULLS NOT DISTINCT"}) + void modelsTemporalKeysAcrossCreateAndAlter(String kind) throws JSQLParserException { + for (String prefix : List.of("CREATE TABLE t (id INT, valid DATERANGE, ", + "ALTER TABLE t ADD ")) { + Statement statement = parse(prefix + "CONSTRAINT temporal_key " + kind + + " (id, valid WITHOUT OVERLAPS) INCLUDE (label) DEFERRABLE INITIALLY DEFERRED" + + (prefix.startsWith("CREATE") ? ", label TEXT)" : ", ADD COLUMN extra INT")); + KeyConstraint index = (KeyConstraint) index(statement); + assertEquals("temporal_key", index.getName()); + assertTrue(statement.toString().contains("CONSTRAINT temporal_key")); + assertEquals(2, index.getColumns().size()); + assertFalse(index.getColumns().get(0).isWithoutOverlaps()); + KeyElement last = index.getColumns().get(1); + assertTrue(last.isWithoutOverlaps()); + assertEquals("valid", last.getColumnName()); + assertNull(last.getOperatorClass()); + assertEquals(List.of("label"), index.getIncludeColumns()); + roundTrip(statement); + last.setWithoutOverlaps(false); + assertFalse(statement.toString().contains("WITHOUT OVERLAPS")); + roundTrip(statement); + } + } + + @ParameterizedTest + @ValueSource(strings = {"", " (id, PERIOD valid)", " (id, PERIOD \"Valid Range\")"}) + void sharesReferencingAndReferencedPeriodLists(String referenced) throws JSQLParserException { + for (String prefix : List.of("CREATE TABLE t (id INT, valid DATERANGE, ", + "ALTER TABLE t ADD ")) { + Statement statement = + parse(prefix + "FOREIGN KEY (id, PERIOD valid) REFERENCES app.parent" + + referenced + " ON DELETE NO ACTION ON UPDATE NO ACTION" + + (prefix.startsWith("CREATE") ? ")" : ", ADD COLUMN extra INT")); + ForeignKeyConstraint foreign = (ForeignKeyConstraint) index(statement); + assertFalse(foreign.getColumns().get(0).isPeriod()); + assertTrue(foreign.getColumns().get(1).isPeriod()); + assertEquals("valid", foreign.getColumns().get(1).getColumnName()); + assertEquals(!referenced.isEmpty(), foreign.getReference().isUsingPeriod()); + assertEquals(Set.of("t", "app.parent"), new TablesNamesFinder().getTables(statement)); + roundTrip(statement); + foreign.getColumns().get(1).setPeriod(false); + foreign.getReference().setUsingPeriod(false); + assertFalse(statement.toString().contains("PERIOD")); + roundTrip(statement); + } + } + + @Test + void preservesPeriodAndEnforcementTogether() throws JSQLParserException { + for (String prefix : List.of("CREATE TABLE t (id INT, valid DATERANGE, ", + "ALTER TABLE t ADD ")) { + Statement statement = parse(prefix + + "CONSTRAINT fk FOREIGN KEY (id, PERIOD valid) REFERENCES p(id, PERIOD valid)" + + " ON DELETE NO ACTION NOT ENFORCED" + + (prefix.startsWith("CREATE") ? ")" : "")); + ForeignKeyConstraint foreign = (ForeignKeyConstraint) index(statement); + assertTrue(foreign.getColumns().get(1).isPeriod()); + assertTrue(foreign.getReference().isUsingPeriod()); + assertEquals(false, foreign.getConstraintAttributes().getEnforced()); + assertNull(foreign.getReference().getConstraintAttributes()); + roundTrip(statement); + foreign.getConstraintAttributes().setEnforced(true); + assertTrue(statement.toString().endsWith(prefix.startsWith("CREATE") + ? " ENFORCED)" + : " ENFORCED")); + roundTrip(statement); + } + } + + @Test + void retainsQuotedAndUnquotedIdentifiersAndLegacyProjections() throws JSQLParserException { + CreateTable table = (CreateTable) parse("CREATE TABLE t (id INT, period DATERANGE, " + + "FOREIGN KEY (id, period) REFERENCES p(id, period))"); + ForeignKeyConstraint foreign = (ForeignKeyConstraint) index(table); + assertFalse(foreign.getColumns().get(1).isPeriod()); + assertFalse(foreign.getReference().isUsingPeriod()); + roundTrip(table); + table = (CreateTable) parse("CREATE TABLE t (id INT, \"PERIOD\" DATERANGE, " + + "FOREIGN KEY (id, PERIOD \"PERIOD\") REFERENCES p(id, PERIOD \"PERIOD\"))"); + foreign = (ForeignKeyConstraint) index(table); + assertEquals(List.of("id", "\"PERIOD\""), foreign.getReferencedColumnNames()); + foreign.getReferencedColumnNames().set(1, "replacement"); + assertTrue(table.toString().contains("PERIOD replacement")); + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void preservesNamedUniqueConstraintsWithAndWithoutTemporalKeys() throws JSQLParserException { + Statement ordinary = parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE (id)"); + assertEquals("uq", index(ordinary).getName()); + assertEquals("ALTER TABLE t ADD CONSTRAINT uq UNIQUE (id)", ordinary.toString()); + roundTrip(ordinary); + Statement temporal = parse("ALTER TABLE t ADD CONSTRAINT uq " + + "UNIQUE (id, valid WITHOUT OVERLAPS)"); + assertEquals("uq", index(temporal).getName()); + assertTrue(((KeyColumnSource) index(temporal)).getColumns().get(1).isWithoutOverlaps()); + roundTrip(temporal); + } + + @Test + void keepsOrdinaryIndexesAndCrossDialectForeignKeys() throws JSQLParserException { + String mysql = "CREATE TABLE t (id INT, FOREIGN KEY (id) REFERENCES p (id))"; + assertNotNull(CCJSqlParserUtil.parse(mysql)); + assertEquals("valid WITHOUT OVERLAPS", + new KeyElement("valid").withWithoutOverlaps(true).toString()); + assertEquals("PERIOD valid", new KeyElement("valid").withPeriod(true).toString()); + assertThrows(JSQLParserException.class, + () -> parse("CREATE INDEX ix ON t (id, valid WITHOUT OVERLAPS)")); + } + + @ParameterizedTest + @ValueSource(strings = {"UNIQUE (valid WITHOUT OVERLAPS, id)", + "PRIMARY KEY (id, valid WITHOUT)", + "UNIQUE (id, valid WITHOUT OVERLAPS WITHOUT OVERLAPS)", + "FOREIGN KEY (PERIOD valid, id) REFERENCES p(id, valid)", + "FOREIGN KEY (id, PERIOD valid) REFERENCES p(PERIOD valid, id)"}) + void rejectsMalformedTemporalMarkers(String constraint) { + assertThrows(JSQLParserException.class, + () -> parse("CREATE TABLE t (id INT, valid DATERANGE, " + constraint + ")")); + } + + private static NamedConstraint index(Statement statement) { + return statement instanceof CreateTable + ? ((CreateTable) statement).getTableConstraints().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getConstraint(); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTextSearchConfigurationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTextSearchConfigurationTest.java new file mode 100644 index 0000000000..2a2f358cfb --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTextSearchConfigurationTest.java @@ -0,0 +1,98 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlTextSearchConfigurationTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE TEXT SEARCH CONFIGURATION cfg (COPY=pg_catalog.english)", + "CREATE TEXT SEARCH CONFIGURATION cfg (COPY='english')", + "CREATE TEXT SEARCH CONFIGURATION app.cfg (PARSER=pg_catalog.default)", + "ALTER TEXT SEARCH CONFIGURATION cfg ADD MAPPING FOR word WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg ADD MAPPING FOR word,asciiword WITH pg_catalog.simple,english_stem", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING FOR word,asciiword WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING REPLACE english_stem WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING FOR word REPLACE english_stem WITH pg_catalog.simple", + "ALTER TEXT SEARCH CONFIGURATION cfg DROP MAPPING FOR word,asciiword", + "ALTER TEXT SEARCH CONFIGURATION cfg DROP MAPPING IF EXISTS FOR word", + "ALTER TEXT SEARCH CONFIGURATION app.cfg RENAME TO new_cfg", + "ALTER TEXT SEARCH CONFIGURATION cfg OWNER TO CURRENT_USER", + "ALTER TEXT SEARCH CONFIGURATION cfg SET SCHEMA app"}) + void configurationAndMappingActionsRoundTrip(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertTrue(statement instanceof CreateTextSearchConfiguration + || statement instanceof AlterTextSearchConfiguration); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + } + + @Test + void sourceAndMappingsCanBeChangedWithoutRawTokens() throws JSQLParserException { + CreateTextSearchConfiguration create = + (CreateTextSearchConfiguration) CCJSqlParserUtil.parse( + "CREATE TEXT SEARCH CONFIGURATION cfg(COPY=pg_catalog.english)"); + assertEquals(CreateTextSearchConfiguration.SourceKind.COPY, create.getSourceKind()); + create.setSourceKind(CreateTextSearchConfiguration.SourceKind.PARSER); + create.setSourceName("pg_catalog.default"); + assertEquals("CREATE TEXT SEARCH CONFIGURATION cfg (PARSER = pg_catalog.default)", + create.toString()); + roundTrip(create); + AlterTextSearchConfiguration alter = (AlterTextSearchConfiguration) CCJSqlParserUtil.parse( + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING FOR word REPLACE english_stem WITH simple"); + assertEquals(AlterTextSearchConfiguration.Action.REPLACE_MAPPING, alter.getAction()); + assertEquals(List.of("word"), alter.getTokenTypes()); + assertEquals("english_stem", alter.getOldDictionary()); + alter.setNewDictionary("pg_catalog.simple"); + alter.getTokenTypes().add("asciiword"); + roundTrip(alter); + alter.getTokenTypes().clear(); + assertEquals( + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING REPLACE english_stem WITH pg_catalog.simple", + alter.toString()); + roundTrip(alter); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TEXT SEARCH CONFIGURATION cfg()", + "CREATE TEXT SEARCH CONFIGURATION cfg(COPY=)", + "CREATE TEXT SEARCH CONFIGURATION cfg(COPY=english,PARSER=default)", + "CREATE OR REPLACE TEXT SEARCH CONFIGURATION cfg(COPY=english)", + "ALTER TEXT SEARCH CONFIGURATION cfg ADD MAPPING WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg DROP MAPPING FOR", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING REPLACE simple WITH", + "ALTER TEXT SEARCH CONFIGURATION cfg ADD MAPPING FOR word WITH simple,"}) + void rejectsIncompleteOrIncompatibleForms(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTypeDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTypeDdlTest.java new file mode 100644 index 0000000000..4db9310631 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTypeDdlTest.java @@ -0,0 +1,216 @@ +/*- + * #%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.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.alter.AlterDomain; +import net.sf.jsqlparser.statement.alter.AlterExtension; +import net.sf.jsqlparser.statement.alter.AlterType; +import net.sf.jsqlparser.statement.create.domain.CreateDomain; +import net.sf.jsqlparser.statement.create.domain.DomainConstraint; +import net.sf.jsqlparser.statement.create.extension.CreateExtension; +import net.sf.jsqlparser.statement.create.type.CreateType; +import net.sf.jsqlparser.statement.create.type.TypeAttribute; +import net.sf.jsqlparser.statement.create.type.EnumTypeDefinition; +import net.sf.jsqlparser.statement.create.type.CompositeTypeDefinition; +import net.sf.jsqlparser.statement.create.type.RangeTypeDefinition; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlTypeDdlTest { + static Stream statements() throws Exception { + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + PostgreSqlTypeDdlTest.class.getResourceAsStream("/postgresql/type-ddl.sql"), + StandardCharsets.UTF_8))) { + return reader.lines() + .filter(line -> !line.trim().isEmpty() && !line.trim().startsWith("--")) + .collect(Collectors.toList()) + .stream(); + } + } + + @ParameterizedTest + @MethodSource("statements") + void testRoundTripAndValidation(String sql) throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql); + assertThat(statement).isInstanceOfAny(CreateType.class, AlterType.class, CreateDomain.class, + AlterDomain.class, CreateExtension.class, AlterExtension.class); + assertThat(CCJSqlParserUtil.parse(statement.toString()).toString()) + .isEqualTo(statement.toString()); + assertThat(statement.getFeatures().getCertain()).contains(StmtFeature.MODIFIES_SCHEMA) + .doesNotContain(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET); + validateNoErrors(sql, 1, DatabaseType.POSTGRESQL); + } + + @Test + void testTypeDefinitionsAndConstruction() throws Exception { + CreateType enumeration = + (CreateType) CCJSqlParserUtil.parse("CREATE TYPE mood AS ENUM ('sad', 'ok')"); + EnumTypeDefinition definition = (EnumTypeDefinition) enumeration.getDefinition(); + assertThat(definition.getLabels()).extracting(StringValue::getValue).containsExactly("sad", + "ok"); + definition.getLabels().add(new StringValue("happy")); + assertThat(enumeration.toString()) + .isEqualTo("CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')"); + + CreateType composite = (CreateType) CCJSqlParserUtil + .parse("CREATE TYPE item AS (name text COLLATE \"C\")"); + TypeAttribute attribute = + ((CompositeTypeDefinition) composite.getDefinition()).getAttributes().get(0); + assertThat(attribute.getName()).isEqualTo("name"); + assertThat(attribute.getDataType().getDataType()).isEqualTo("text"); + assertThat(attribute.getCollation()).isEqualTo("\"C\""); + + CreateType range = (CreateType) CCJSqlParserUtil + .parse("CREATE TYPE r AS RANGE (SUBTYPE = numeric, SUBTYPE_DIFF = app.diff)"); + RangeTypeDefinition rangeDefinition = (RangeTypeDefinition) range.getDefinition(); + assertThat(rangeDefinition.getSubtype().getDataType()).isEqualTo("numeric"); + assertThat(rangeDefinition.getOptions().get(1).getKind()) + .isEqualTo(RangeTypeDefinition.OptionKind.SUBTYPE_DIFF); + assertThat(rangeDefinition.getOptions().get(1).getName()).isEqualTo("app.diff"); + + CreateType constructed = new CreateType(); + constructed.setName("new_mood"); + constructed.setDefinition(definition); + assertThat(CCJSqlParserUtil.parse(constructed.toString()).toString()) + .isEqualTo(constructed.toString()); + } + + @Test + void testTypeAlterations() throws Exception { + AlterType alteration = (AlterType) CCJSqlParserUtil + .parse("ALTER TYPE mood ADD VALUE IF NOT EXISTS 'fine' AFTER 'ok'"); + assertThat(alteration.getAction()).isEqualTo(AlterType.Action.ADD_VALUE); + assertThat(alteration.isIfNotExists()).isTrue(); + assertThat(alteration.getPosition()).isEqualTo(AlterType.Position.AFTER); + assertThat(alteration.getValue().getValue()).isEqualTo("fine"); + assertThat(alteration.getNeighborValue().getValue()).isEqualTo("ok"); + AlterType attributes = (AlterType) CCJSqlParserUtil.parse( + "ALTER TYPE item ADD ATTRIBUTE code text, DROP ATTRIBUTE IF EXISTS price CASCADE"); + assertThat(attributes.getAttributeChanges()).hasSize(2); + assertThat(attributes.getAttributeChanges().get(1).isIfExists()).isTrue(); + assertThat(attributes.getAttributeChanges().get(1).getBehavior()) + .isEqualTo(CascadeBehavior.CASCADE); + } + + @Test + void testDomainConstraintsAndTraversal() throws Exception { + CreateDomain domain = (CreateDomain) CCJSqlParserUtil.parse( + "CREATE DOMAIN positive AS integer DEFAULT 1 CONSTRAINT required NOT NULL CHECK (VALUE > 0)"); + assertThat(domain.isUseAs()).isTrue(); + assertThat(domain.getConstraints()).extracting(DomainConstraint::getKind) + .containsExactly(DomainConstraint.Kind.NOT_NULL, DomainConstraint.Kind.CHECK); + assertThat(domain.getConstraints().get(0).getName()).isEqualTo("required"); + AlterDomain altered = (AlterDomain) CCJSqlParserUtil.parse( + "ALTER DOMAIN positive ADD CONSTRAINT positive_check CHECK (VALUE > 1) NOT VALID"); + assertThat(altered.isNotValid()).isTrue(); + assertThat(altered.getConstraint().getExpression()).isNotNull(); + + List names = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + assertThat(context).isEqualTo("domain"); + names.add(column.getColumnName()); + return null; + } + }; + StatementVisitorAdapter visitor = + new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)); + domain.accept(visitor, "domain"); + altered.accept(visitor, "domain"); + assertThat(names).containsExactly("VALUE", "VALUE"); + assertThat(TablesNamesFinder.findTables(domain.toString())).isEmpty(); + } + + @Test + void testDomainExpressionDeparser() throws Exception { + for (String sql : new String[] {"CREATE DOMAIN positive AS integer CHECK (VALUE > 0)", + "ALTER DOMAIN positive ADD CHECK (VALUE > 0) NOT VALID"}) { + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("new_").append(column.getColumnName()); + } + }; + Statement statement = CCJSqlParserUtil.parse(sql); + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output)); + assertThat(output.toString()).contains("new_VALUE"); + assertThat(statement.toString()).doesNotContain("new_VALUE"); + CCJSqlParserUtil.parse(output.toString()); + } + } + + @Test + void testExtensionOptionsAndRoutineSignatures() throws Exception { + CreateExtension extension = (CreateExtension) CCJSqlParserUtil.parse( + "CREATE EXTENSION IF NOT EXISTS hstore WITH VERSION '1.8' SCHEMA public CASCADE"); + assertThat(extension.isIfNotExists()).isTrue(); + assertThat(extension.isUseWith()).isTrue(); + assertThat(extension.getOptions()).extracting(CreateExtension.Option::getKind) + .containsExactly(CreateExtension.OptionKind.VERSION, + CreateExtension.OptionKind.SCHEMA, CreateExtension.OptionKind.CASCADE); + assertThat(extension.getOptions().get(0).getVersion()).isInstanceOf(StringValue.class); + AlterExtension alteration = (AlterExtension) CCJSqlParserUtil + .parse("ALTER EXTENSION hstore ADD FUNCTION app.f(integer, text[])"); + assertThat(alteration.getMember().getRoutine().getArguments()).hasSize(2); + assertThat(alteration.getMember().getRoutine().getArguments().get(1).getDataType() + .getArrayData()).hasSize(1); + assertThat(TablesNamesFinder.findTables(alteration.toString())).isEmpty(); + assertThat(TablesNamesFinder.findTables("ALTER EXTENSION hstore ADD TABLE app.settings")) + .containsExactly("app.settings"); + assertThat(TablesNamesFinder.findTables("CREATE TYPE item AS (id integer)")).isEmpty(); + assertThat(TablesNamesFinder.findTables("ALTER TYPE item SET SCHEMA app")).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE OR REPLACE TYPE t AS ENUM ('a')", + "CREATE OR REPLACE DOMAIN d AS integer", "CREATE OR REPLACE EXTENSION e", + "CREATE TYPE r AS RANGE (UNKNOWN = integer)", + "CREATE TYPE r AS RANGE (CANONICAL = f)", + "ALTER TYPE mood ADD VALUE ok", "ALTER TYPE item ADD ATTRIBUTE x", + "CREATE DOMAIN positive AS integer CHECK ()", + "ALTER DOMAIN positive ADD NULL", "ALTER DOMAIN positive ADD NOT NULL NOT VALID", + "CREATE EXTENSION hstore FROM '1.0'", "ALTER EXTENSION hstore ADD UNKNOWN app.x", + "ALTER EXTENSION hstore ADD FUNCTION f(*)", "ALTER EXTENSION hstore ADD AGGREGATE f"}) + void testInvalidSyntax(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/RedshiftCreateViewTest.java b/src/test/java/net/sf/jsqlparser/statement/create/RedshiftCreateViewTest.java new file mode 100644 index 0000000000..d5346390da --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/RedshiftCreateViewTest.java @@ -0,0 +1,82 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.view.AutoRefreshOption; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class RedshiftCreateViewTest { + @ParameterizedTest + @ValueSource(strings = {"", " BACKUP YES", " BACKUP NO"}) + void preserveBackupAndOmission(String clause) throws JSQLParserException { + String sql = "CREATE MATERIALIZED VIEW myview" + clause + " AS SELECT * FROM mytab"; + CreateView view = (CreateView) TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + assertEquals(clause.isEmpty() ? null : clause.endsWith("YES"), view.getBackup()); + CreateView reparsed = (CreateView) CCJSqlParserUtil.parse(view.toString()); + assertEquals(view.getBackup(), reparsed.getBackup()); + assertTrue(TablesNamesFinder.findTables(sql).contains("mytab")); + } + + @ParameterizedTest + @ValueSource(strings = {"YES", "NO"}) + void combineWithAutoRefresh(String value) throws JSQLParserException { + CreateView view = (CreateView) TestUtils.assertSqlCanBeParsedAndDeparsed( + "CREATE MATERIALIZED VIEW report.mv BACKUP NO AUTO REFRESH " + value + + " AS SELECT * FROM source"); + assertEquals(Boolean.FALSE, view.getBackup()); + assertEquals(AutoRefreshOption.from(value), view.getAutoRefresh()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE MATERIALIZED VIEW mv BACKUP AS SELECT 1", + "CREATE MATERIALIZED VIEW mv BACKUP TRUE AS SELECT 1", + "CREATE MATERIALIZED VIEW mv BACKUP YES BACKUP NO AS SELECT 1", + "CREATE MATERIALIZED VIEW mv AUTO REFRESH YES BACKUP NO AS SELECT 1", + "CREATE VIEW mv BACKUP NO AS SELECT 1" + }) + void rejectMalformedOptions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withUnsupportedStatements(false))); + } + + @Test + void constructAndClearBackup() throws JSQLParserException { + CreateView view = new CreateView().withMaterialized(true).withBackup(false) + .withView(new Table("mv")) + .withSelect((Select) CCJSqlParserUtil.parse("SELECT * FROM source")); + TestUtils.assertStatementCanBeDeparsedAs(view, + "CREATE MATERIALIZED VIEW mv BACKUP NO AS SELECT * FROM source"); + view.setBackup(null); + TestUtils.assertStatementCanBeDeparsedAs(view, + "CREATE MATERIALIZED VIEW mv AS SELECT * FROM source"); + view.setMaterialized(false); + view.setBackup(true); + assertThrows(IllegalArgumentException.class, view::validateOptions); + } + + @Test + void backupRemainsAnIdentifier() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "CREATE MATERIALIZED VIEW backup BACKUP YES AS SELECT backup FROM backup"); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT backup FROM backup"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/SqlRoutineBodyBoundaryTest.java b/src/test/java/net/sf/jsqlparser/statement/create/SqlRoutineBodyBoundaryTest.java new file mode 100644 index 0000000000..1461d64894 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/SqlRoutineBodyBoundaryTest.java @@ -0,0 +1,193 @@ +/*- + * #%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.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserConstants; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.CreateFunctionalStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.function.CreateFunction; +import net.sf.jsqlparser.statement.create.procedure.CreateProcedure; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlRoutineBodyBoundaryTest { + private static final String FOLLOWING = "SELECT 2718 AS after_routine"; + + static Stream sqlRoutines() { + Stream postgres = Stream.of( + "CREATE FUNCTION f(x int) RETURNS int LANGUAGE SQL IMMUTABLE RETURN x + 1", + "CREATE FUNCTION f(x int) RETURNS int LANGUAGE SQL RETURN " + + "CASE WHEN x IS NULL THEN 0 ELSE x END + CASE WHEN x > 0 THEN 1 ELSE 0 END", + "CREATE FUNCTION f(x int) RETURNS boolean LANGUAGE SQL RETURN x IS NULL", + "CREATE FUNCTION f(x begin) RETURNS begin LANGUAGE SQL RETURN x", + "CREATE FUNCTION f(x public.begin) RETURNS public.begin LANGUAGE SQL RETURN x", + "CREATE FUNCTION f() RETURNS int LANGUAGE SQL BEGIN ATOMIC " + + "SELECT t.end, t.case FROM t; SELECT 42; END", + "CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN (SELECT max(id) FROM hidden)", + "CREATE FUNCTION f(x int) RETURNS int LANGUAGE SQL BEGIN ATOMIC " + + "SELECT CASE WHEN x > 0 THEN CASE WHEN x > 1 THEN x ELSE 1 END ELSE 0 END; " + + "SELECT x + 1; END", + "CREATE PROCEDURE p() LANGUAGE SQL BEGIN ATOMIC " + + "INSERT INTO t VALUES (CASE WHEN true THEN 1 ELSE 0 END); SELECT 2; END", + "CREATE FUNCTION f() RETURNS int SET search_path TO public " + + "AS 'SELECT 1;' LANGUAGE sql") + .map(sql -> Arguments.of(Dialect.POSTGRESQL, sql)); + Stream mysql = Stream.of( + "CREATE PROCEDURE p() SELECT 1", + "CREATE PROCEDURE p() SET @result = IF(1, 2, 3)", + "CREATE PROCEDURE p() INSERT INTO t VALUES (1)", + "CREATE FUNCTION f(x int) RETURNS int DETERMINISTIC RETURN x + 1", + "CREATE FUNCTION f(x int) RETURNS int RETURN CASE WHEN x IS NULL THEN 0 ELSE x END", + "CREATE PROCEDURE p() BEGIN SELECT CASE WHEN 1 THEN IF(1, 2, 3) ELSE 0 END; " + + "BEGIN SELECT 'END; SELECT 0;'; END; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN IF (1) THEN SELECT 1; " + + "ELSEIF 2 THEN SELECT 2; ELSE SELECT 3; END IF; SELECT 4; END", + "CREATE PROCEDURE p() BEGIN WHILE (1) DO IF (1) THEN SELECT 1; END IF; " + + "END WHILE; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN REPEAT SELECT 1; " + + "UNTIL CASE WHEN 1 THEN true ELSE false END END REPEAT; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN CASE 1 WHEN 1 THEN IF (1) THEN SELECT 1; END IF; " + + "ELSE SELECT 2; END CASE; SELECT 3; END", + "CREATE PROCEDURE p() outer_block: BEGIN outer_loop: LOOP " + + "IF (1) THEN LEAVE outer_loop; END IF; END LOOP outer_loop; " + + "SELECT 2; END outer_block", + "CREATE PROCEDURE p() IF (1) THEN SELECT 1; ELSE SELECT 2; END IF", + "CREATE PROCEDURE p() CASE WHEN 1 THEN SELECT 1; ELSE SELECT 2; END CASE", + "CREATE PROCEDURE p() BEGIN DECLARE CONTINUE HANDLER FOR NOT FOUND " + + "BEGIN SET @done = 1; END; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN DECLARE CONTINUE HANDLER FOR NOT FOUND " + + "IF @done IS NULL THEN SET @done = 1; END IF; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN DECLARE CONTINUE HANDLER FOR NOT FOUND " + + "SET @done = IF(1, 2, 3); SELECT 2; END") + .map(sql -> Arguments.of(Dialect.MYSQL, sql)); + return Stream.concat(postgres, mysql); + } + + @ParameterizedTest + @MethodSource("sqlRoutines") + void preservesRoutineBodyAndFollowingStatement(Dialect dialect, String sql) throws Exception { + String script = sql + "; " + FOLLOWING + ";"; + Statements statements = parse(script, dialect); + assertRoutineAndFollowing(statements, sql); + assertRoutineAndFollowing(parser(script, dialect).Statements(), sql); + assertRoutineAndFollowing(CCJSqlParserUtil.parseStatements(script), sql); + assertRoutineAndFollowing(parse(statements.toString(), dialect), sql); + + StringBuilder deparsed = new StringBuilder(); + for (Statement statement : statements) { + statement.accept(new StatementDeParser(deparsed), null); + deparsed.append(";\n"); + } + assertRoutineAndFollowing(parse(deparsed.toString(), dialect), sql); + assertThat(parse(deparsed.toString(), dialect).get(0).toString()) + .isEqualTo(statements.get(0).toString()); + assertThat(CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)).toString()) + .isEqualTo(statements.get(0).toString().replaceFirst(";$", "")); + } + + @Test + void directSingleStatementParserLeavesFollowingStatementAvailable() throws Exception { + // walking a script statement by statement is SingleStatement()'s job; Statement() reads + // the input as exactly one statement and rejects whatever is left over + CCJSqlParser parser = parser("CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1; " + + FOLLOWING + ";", Dialect.POSTGRESQL); + assertThat(parser.SingleStatement()).isInstanceOf(CreateFunction.class); + assertThat(parser.getNextToken().kind).isEqualTo(CCJSqlParserConstants.ST_SEMICOLON); + assertThat(parser.getToken(1).kind).isEqualTo(CCJSqlParserConstants.K_SELECT); + assertThat(parser.SingleStatement().toString()).isEqualTo(FOLLOWING); + assertThat(parser.getNextToken().kind).isEqualTo(CCJSqlParserConstants.ST_SEMICOLON); + assertThat(parser.getToken(1).kind).isEqualTo(CCJSqlParserConstants.EOF); + } + + @Test + void keepsAdjacentRoutinesAndDdlSeparate() throws Exception { + String sql = "CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1;" + + "CREATE PROCEDURE p() LANGUAGE SQL BEGIN ATOMIC SELECT CASE WHEN true " + + "THEN 1 ELSE 0 END; SELECT 2; END; CREATE TABLE after_routines(id int);" + + FOLLOWING + ";"; + Statements statements = parse(sql, Dialect.POSTGRESQL); + assertThat(statements).hasSize(4); + assertThat(statements.get(0)).isExactlyInstanceOf(CreateFunction.class); + assertThat(statements.get(1)).isExactlyInstanceOf(CreateProcedure.class); + assertThat(statements.get(2).toString()).isEqualTo("CREATE TABLE after_routines (id int)"); + assertThat(statements.get(3).toString()).isEqualTo(FOLLOWING); + assertThat(parse(statements.toString(), Dialect.POSTGRESQL)).hasSize(4); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE FUNCTION f() RETURNS int LANGUAGE SQL BEGIN ATOMIC SELECT 1;", + "CREATE FUNCTION f() RETURNS int LANGUAGE SQL BEGIN ATOMIC " + + "SELECT CASE WHEN true THEN 1 END; SELECT 2;", + "CREATE PROCEDURE p() BEGIN IF 1 THEN SELECT 1; END", + "CREATE PROCEDURE p() BEGIN SELECT CASE WHEN 1 THEN 2; END" + }) + void rejectsUnterminatedBlocks(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parseStatements(sql)); + } + + @Test + void reportsInvalidFollowingStatementInsteadOfCapturingIt() { + assertThrows(JSQLParserException.class, + () -> parse("CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1; SELEC 2;", + Dialect.POSTGRESQL)); + assertThrows(JSQLParserException.class, + () -> parse("CREATE PROCEDURE p() SELECT 1; SELEC 2;", Dialect.MYSQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE PROCEDURE p AS v int; BEGIN NULL; END", + "CREATE FUNCTION f(x int) RETURN int IS v int; " + + "BEGIN IF x > 0 THEN RETURN x; END IF; RETURN 0; END", + "CREATE PROCEDURE p AS BEGIN FOR i IN 1 .. 3 LOOP NULL; END LOOP; NULL; END", + "CREATE PROCEDURE p AS BEGIN WHILE 1 = 1 LOOP NULL; END LOOP; NULL; END" + }) + void retainsOracleDeclarationAndBlockBoundaries(String sql) throws Exception { + assertRoutineAndFollowing(CCJSqlParserUtil.parseStatements(sql + "; " + FOLLOWING + ";"), + sql); + } + + private static CCJSqlParser parser(String sql, Dialect dialect) { + return CCJSqlParserUtil.newParser(sql).withDialect(dialect) + .withUnsupportedStatements(false); + } + + private static Statements parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parseStatements(sql, + parser -> parser.withDialect(dialect).withUnsupportedStatements(false)); + } + + private static void assertRoutineAndFollowing(Statements statements, String source) { + assertThat(statements).hasSize(2); + assertThat(statements.get(0)).isInstanceOf(CreateFunctionalStatement.class); + CreateFunctionalStatement routine = (CreateFunctionalStatement) statements.get(0); + assertThat(routine.getFunctionDeclarationParts()).doesNotContain("after_routine", "2718"); + // The entire body must survive, including statements after an inner END. + assertThat(routine.toString().replaceAll("\\s+", "").replaceFirst(";$", "")) + .isEqualTo(source.replaceAll("\\s+", "")); + assertThat(statements.get(1)).isExactlyInstanceOf(PlainSelect.class); + assertThat(statements.get(1).toString()).isEqualTo(FOLLOWING); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/SqlServerConstraintClusteringTest.java b/src/test/java/net/sf/jsqlparser/statement/create/SqlServerConstraintClusteringTest.java new file mode 100644 index 0000000000..fdea22d6bf --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/SqlServerConstraintClusteringTest.java @@ -0,0 +1,131 @@ +/*- + * #%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.statement.create; + +import net.sf.jsqlparser.statement.create.table.IndexOptions; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class SqlServerConstraintClusteringTest { + private static KeyConstraint constraint(Statement statement) { + return (KeyConstraint) (statement instanceof CreateTable + ? ((CreateTable) statement).getTableConstraints().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getConstraint()); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.SQLSERVER)); + } + + @Test + void parsesSakilaReproducerIssue1589() throws Exception { + String sql = "CREATE TABLE actor (actor_id INT NOT NULL IDENTITY, " + + "first_name VARCHAR (45) NOT NULL, last_name VARCHAR (45) NOT NULL, " + + "last_update DATETIME NOT NULL, PRIMARY KEY NONCLUSTERED (actor_id))"; + KeyConstraint index = constraint(TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.SQLSERVER))); + assertEquals(IndexOptions.Clustering.NONCLUSTERED, index.getClustering()); + assertNull(((KeyConstraint) index).getIndexName()); + assertEquals(List.of("actor_id"), index.getColumnsNames()); + } + + @ParameterizedTest + @EnumSource(IndexOptions.Clustering.class) + void sharesClusteringAcrossCreateAndAlter(IndexOptions.Clustering clustering) throws Exception { + for (String type : List.of("PRIMARY KEY", "UNIQUE")) { + for (String name : List.of("", "CONSTRAINT [key name] ")) { + String definition = name + type + " " + clustering + " (id)"; + for (String sql : List.of("CREATE TABLE t (id INT, " + definition + ")", + "ALTER TABLE t ADD " + definition)) { + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.SQLSERVER)); + KeyConstraint index = constraint(statement); + assertEquals(clustering, index.getClustering(), sql); + assertEquals(type, index.getType()); + assertEquals(name.isEmpty() ? null : "[key name]", index.getName()); + if (index instanceof KeyConstraint) { + assertNull(((KeyConstraint) index).getIndexName()); + } + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(clustering, constraint(parse(output.toString())).getClustering()); + assertEquals(clustering, + constraint(parse(statement.toString())).getClustering()); + } + } + } + } + + @Test + void preservesNamesOutsideSqlServerAndQuotedNames() throws Exception { + for (String type : List.of("PRIMARY KEY", "UNIQUE")) { + String sql = "CREATE TABLE t (id INT, " + type + " NONCLUSTERED (id))"; + KeyConstraint defaultIndex = + (KeyConstraint) constraint(CCJSqlParserUtil.parse(sql)); + assertNull(defaultIndex.getClustering()); + assertEquals("NONCLUSTERED", defaultIndex.getIndexName()); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.SQLSERVER) { + KeyConstraint index = (KeyConstraint) constraint(CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(dialect))); + assertNull(index.getClustering(), dialect.name()); + assertEquals("NONCLUSTERED", index.getIndexName()); + } + } + KeyConstraint quoted = (KeyConstraint) constraint( + parse("CREATE TABLE t (id INT, " + type + " [NONCLUSTERED] (id))")); + assertNull(quoted.getClustering()); + assertEquals("[NONCLUSTERED]", quoted.getIndexName()); + } + } + + @Test + void supportsMutationAndLeavesOmittedOptionUnspecified() throws Exception { + Statement statement = parse("CREATE TABLE t (id INT, PRIMARY KEY (id))"); + KeyConstraint index = constraint(statement); + assertNull(index.getClustering()); + index.setClustering(IndexOptions.Clustering.CLUSTERED); + TestUtils.assertDeparse(statement, "CREATE TABLE t (id INT, PRIMARY KEY CLUSTERED (id))"); + index.setClustering(null); + TestUtils.assertDeparse(statement, "CREATE TABLE t (id INT, PRIMARY KEY (id))"); + KeyConstraint built = new KeyConstraint().withType("UNIQUE").withName("uq_t") + .withClustering(IndexOptions.Clustering.NONCLUSTERED) + .withColumnsNames(List.of("id")); + assertEquals("CONSTRAINT uq_t UNIQUE NONCLUSTERED (id)", built.toString()); + } + + @Test + void keepsFollowingConstraintsAndRejectsDuplicateModifiers() throws Exception { + CreateTable table = (CreateTable) parse("CREATE TABLE t (id INT, other_id INT, " + + "PRIMARY KEY NONCLUSTERED (id), UNIQUE (other_id))"); + assertNull(((KeyConstraint) table.getTableConstraints().get(1)).getClustering()); + for (String sql : List.of( + "CREATE TABLE t (id INT, PRIMARY KEY NONCLUSTERED CLUSTERED (id))", + "ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY CLUSTERED NONCLUSTERED (id)", + "ALTER TABLE t ADD UNIQUE NONCLUSTERED")) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/SqlServerCreateTableSeparatorTest.java b/src/test/java/net/sf/jsqlparser/statement/create/SqlServerCreateTableSeparatorTest.java new file mode 100644 index 0000000000..96db28af94 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/SqlServerCreateTableSeparatorTest.java @@ -0,0 +1,91 @@ +/*- + * #%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.statement.create; + +import net.sf.jsqlparser.statement.create.table.IndexOptions; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class SqlServerCreateTableSeparatorTest { + private CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.SQLSERVER).withUnsupportedStatements(false)); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id int,)", + "CREATE TABLE t (id int, name varchar(20),)", + "CREATE TABLE t (id int, PRIMARY KEY (id),)", + "CREATE TABLE t (id int, CHECK (id > 0),)", + "CREATE TABLE t (id int, /* last element */ )"}) + void acceptsOneTrailingSeparatorAndNormalizesBothRenderers(String sql) throws Exception { + CreateTable table = parse(sql); + String normalized = table.toString(); + assertFalse(normalized.contains(",)")); + assertEquals(normalized, parse(normalized).toString()); + StringBuilder buffer = new StringBuilder(); + table.accept(new StatementDeParser(buffer), null); + assertEquals(normalized, buffer.toString()); + assertEquals(table.getTableElements().size(), + parse(buffer.toString()).getTableElements().size()); + } + + @Test + void preservesSakilaColumnsAndNonclusteredPrimaryKey() throws Exception { + CreateTable table = parse("CREATE TABLE film_text (film_id INT NOT NULL, " + + "title VARCHAR(255) NOT NULL, description TEXT, PRIMARY KEY NONCLUSTERED (film_id),)"); + assertEquals(4, table.getTableElements().size()); + assertInstanceOf(ColumnDefinition.class, table.getTableElements().get(0)); + KeyConstraint primaryKey = + assertInstanceOf(KeyConstraint.class, table.getTableElements().get(3)); + assertEquals("PRIMARY KEY", primaryKey.getType()); + assertEquals(IndexOptions.Clustering.NONCLUSTERED, primaryKey.getClustering()); + assertEquals(3, table.getColumnDefinitions().size()); + table.getColumnDefinitions().get(0).setColumnName("renamed_id"); + assertTrue(table.toString().contains("renamed_id INT")); + assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1;", + p -> p.withDialect(Dialect.SQLSERVER)).size()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (,)", + "CREATE TABLE t (, id int)", "CREATE TABLE t (id int,,)", + "CREATE TABLE t (id int,, name int)", "CREATE TABLE t (id int, PRIMARY KEY,)", + "CREATE TABLE t (id int, PRIMARY KEY (id,))", + "CREATE FUNCTION f() RETURNS @r TABLE (id int,) AS BEGIN RETURN; END"}) + void rejectsMissingElementsAndDoesNotWidenOtherDefinitionLists(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void retainsOtherDialectsAndCreateTableForms() throws Exception { + String trailing = "CREATE TABLE t (id int,)"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(trailing)); + for (Dialect dialect : new Dialect[] {Dialect.POSTGRESQL, Dialect.MYSQL, Dialect.ORACLE}) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(trailing, p -> p.withDialect(dialect))); + } + for (String sql : new String[] {"CREATE TABLE t (id int, PRIMARY KEY (id))", + "CREATE TABLE t (id) AS SELECT 1", "CREATE TABLE t AS SELECT 1", + "CREATE TABLE t ()"}) { + assertEquals(CCJSqlParserUtil.parse(sql).toString(), parse(sql).toString()); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/StatementIdentifierTest.java b/src/test/java/net/sf/jsqlparser/statement/create/StatementIdentifierTest.java new file mode 100644 index 0000000000..161ff7f4ea --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/StatementIdentifierTest.java @@ -0,0 +1,49 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class StatementIdentifierTest { + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"POSTGRESQL", "MYSQL"}) + void statementIsAnUnquotedIdentifier(Dialect dialect) throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE statement (statement INT)", p -> p.withDialect(dialect)); + assertEquals("statement", table.getTable().getName()); + assertEquals("statement", table.getColumnDefinitions().get(0).getColumnName()); + for (String sql : new String[] {"SELECT statement FROM statement", + "ALTER TABLE statement ADD COLUMN statement INT"}) { + Statement statement = CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(sql, buffer.toString()); + } + } + + @Test + void triggerOrientationRemainsAKeyword() throws JSQLParserException { + CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse( + "CREATE TRIGGER tr AFTER INSERT ON t FOR EACH STATEMENT EXECUTE FUNCTION f()", + p -> p.withDialect(Dialect.POSTGRESQL)); + assertEquals(CreateTrigger.Orientation.STATEMENT, trigger.getOrientation()); + assertTrue(trigger.toString().contains("FOR EACH STATEMENT")); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/TableElementMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/TableElementMutationTest.java new file mode 100644 index 0000000000..d00bdf0b29 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/TableElementMutationTest.java @@ -0,0 +1,114 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.LikeClause; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; + +class TableElementMutationTest { + private static final String SQL = "CREATE TABLE t (LIKE parent INCLUDING DEFAULTS, " + + "a INT, CONSTRAINT c CHECK (a > 0), b INT)"; + + @Test + void legacyListEditsUpdateOrderedElements() throws JSQLParserException { + CreateTable table = parse(); + table.getColumnDefinitions().remove(1); + table.getTableConstraints().clear(); + table.getColumnDefinitions().add(0, column("first")); + table.getColumnDefinitions().set(1, column("last")); + assertSql(table, "CREATE TABLE t (LIKE parent INCLUDING DEFAULTS, first INT, last INT)"); + assertEquals(3, table.getTableElements().size()); + assertTrue(new TablesNamesFinder().getTables(table).contains("parent")); + } + + @Test + void orderedListEditsRemainVisibleThroughExistingViews() throws JSQLParserException { + CreateTable table = parse(); + List columns = table.getColumnDefinitions(); + table.getTableElements().remove(1); + assertEquals("b", columns.get(0).getColumnName()); + table.getTableElements().add(column("extra")); + assertEquals(2, columns.size()); + columns.clear(); + assertEquals(2, table.getTableElements().size()); + assertEquals(1, table.getTableConstraints().size()); + assertEquals(1, table.getTableElements(LikeClause.class).size()); + } + + @Test + void replacingColumnsPreservesOtherElementsAndTheirPositions() throws JSQLParserException { + CreateTable table = parse(); + table.setColumnDefinitions(new ArrayList<>(table.getColumnDefinitions())); + assertSql(table, SQL); + table.setColumnDefinitions(Collections.singletonList(column("replacement"))); + assertSql(table, "CREATE TABLE t (LIKE parent INCLUDING DEFAULTS, replacement INT, " + + "CONSTRAINT c CHECK (a > 0))"); + table.setTableConstraints(null); + table.setColumnDefinitions(null); + assertSql(table, "CREATE TABLE t (LIKE parent INCLUDING DEFAULTS)"); + } + + @Test + void fluentAddersCanPassTheirOwnViewsToSetters() throws JSQLParserException { + CreateTable table = parse(); + table.addColumnDefinitions(column("extra")); + assertEquals(3, table.getColumnDefinitions().size()); + assertEquals(1, table.getTableElements(LikeClause.class).size()); + table.setTableConstraints(table.getTableConstraints()); + assertEquals(1, table.getTableConstraints().size()); + assertSql(table, SQL.substring(0, SQL.length() - 1) + ", extra INT)"); + } + + @Test + void filteredListHonorsIndexBounds() throws JSQLParserException { + List columns = parse().getColumnDefinitions(); + assertThrows(IndexOutOfBoundsException.class, () -> columns.get(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> columns.get(2)); + assertThrows(IndexOutOfBoundsException.class, () -> columns.add(3, column("bad"))); + } + + @Test + void clearingAllElementsKeepsBothRenderersConsistent() throws JSQLParserException { + CreateTable table = parse(); + table.getTableElements().clear(); + assertSql(table, "CREATE TABLE t ()"); + assertTrue(table.getColumnDefinitions().isEmpty()); + assertTrue(table.getTableConstraints().isEmpty()); + } + + private static CreateTable parse() throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(SQL); + } + + private static ColumnDefinition column(String name) { + return new ColumnDefinition(name, new ColDataType("INT")); + } + + private static void assertSql(CreateTable table, String expected) { + assertEquals(expected, table.toString()); + StringBuilder buffer = new StringBuilder(); + table.accept(new StatementDeParser(buffer), null); + assertEquals(expected, buffer.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/TableOptionMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/TableOptionMutationTest.java new file mode 100644 index 0000000000..31a957bf14 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/TableOptionMutationTest.java @@ -0,0 +1,69 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.ArrayList; +import java.util.Arrays; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; + +class TableOptionMutationTest { + @Test + void changingTypedOptionUpdatesLegacyTokensAndBothRenderers() throws JSQLParserException { + CreateTable table = parse(); + TableOption engine = table.getTableOption(TableOption.Kind.ENGINE).orElseThrow(); + engine.setValue("MyISAM"); + engine.setUseEquals(false); + assertEquals(Arrays.asList("ENGINE", "MyISAM"), table.getTableOptionsStrings()); + assertSql(table, "CREATE TABLE t (id INT) ENGINE MyISAM"); + } + + @Test + void addingAndClearingTypedOptionsUpdatesDeparser() throws JSQLParserException { + CreateTable table = parse(); + table.getTableOptions().add(TableOption.raw("ROW_FORMAT", "=", "DYNAMIC")); + assertSql(table, "CREATE TABLE t (id INT) ENGINE = InnoDB ROW_FORMAT = DYNAMIC"); + table.getTableOptions().remove(0); + assertSql(table, "CREATE TABLE t (id INT) ROW_FORMAT = DYNAMIC"); + table.getTableOptions().clear(); + assertSql(table, "CREATE TABLE t (id INT)"); + } + + @Test + void rawOptionsRemainMutableAndCanReplaceTypedOptions() throws JSQLParserException { + CreateTable table = parse(); + table.setTableOptionsStrings(new ArrayList<>(Arrays.asList("ENGINE", "=", "CSV"))); + assertNull(table.getTableOptions()); + table.getTableOptionsStrings().set(2, "MyISAM"); + assertSql(table, "CREATE TABLE t (id INT) ENGINE = MyISAM"); + table.setTableOptions(null); + assertNull(table.getTableOptionsStrings()); + assertSql(table, "CREATE TABLE t (id INT)"); + } + + private static CreateTable parse() throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t (id INT) ENGINE=InnoDB"); + } + + private static void assertSql(CreateTable table, String expected) throws JSQLParserException { + assertEquals(expected, table.toString()); + StringBuilder buffer = new StringBuilder(); + table.accept(new StatementDeParser(buffer), null); + assertEquals(expected, buffer.toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/TableOptionStateTest.java b/src/test/java/net/sf/jsqlparser/statement/create/TableOptionStateTest.java new file mode 100644 index 0000000000..4261e65b11 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/TableOptionStateTest.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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpressionTableOption; +import net.sf.jsqlparser.statement.alter.AlterOperation; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignTableOptions; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class TableOptionStateTest { + @Test + void replacingTokensUpdatesCreateLookupAndAlterOperation() throws JSQLParserException { + CreateTable create = create("ENGINE=InnoDB", Dialect.MYSQL); + Alter alter = (Alter) parse("ALTER TABLE t ENGINE=InnoDB", Dialect.MYSQL); + AlterExpressionTableOption action = + (AlterExpressionTableOption) alter.getAlterExpressions().get(0); + for (TableOption option : List.of(create.getTableOptions().get(0), + action.getStructuredTableOption())) { + List raw = new ArrayList<>(List.of("ROW_FORMAT", "=", "DYNAMIC")); + option.setTokens(raw); + assertSame(raw, option.getTokens()); + assertEquals(TableOption.Kind.OTHER, option.getKind()); + assertNull(option.getName()); + assertNull(option.getValue()); + assertFalse(option.isUseEquals()); + raw.set(2, "COMPRESSED"); + assertEquals("ROW_FORMAT = COMPRESSED", option.toString()); + } + assertTrue(create.getTableOption(TableOption.Kind.ENGINE).isEmpty()); + assertTrue(create.getTableOption(TableOption.Kind.ROW_FORMAT).isEmpty()); + assertSame(create.getTableOptions().get(0), + create.getTableOption(TableOption.Kind.OTHER).orElseThrow()); + assertEquals(AlterOperation.SET_TABLE_OPTION, action.getOperation()); + assertNull(action.getEngineOption()); + assertRoundTrip(create, Dialect.MYSQL); + assertRoundTrip(alter, Dialect.MYSQL); + } + + @ParameterizedTest + @MethodSource("structuredSetters") + void eachStructuredSetterLeavesOpaqueTokens(Consumer setter) { + TableOption option = TableOption.raw("CUSTOM", "opaque text"); + setter.accept(option); + assertFalse(option.getTokens().contains("CUSTOM")); + assertFalse(option.getTokens().contains("opaque text")); + } + + private static Stream> structuredSetters() { + return Stream.of(option -> option.setKind(TableOption.Kind.COMMENT), + option -> option.setName("COMMENT"), option -> option.setValue("'new'"), + option -> option.setUseEquals(true), + option -> option.setTablespaceStorage(ColumnOption.Storage.MEMORY)); + } + + @Test + void rawToStructuredEditsAreOrderIndependentAndDoNotRestoreOldValues() + throws JSQLParserException { + CreateTable create = create("ENGINE=InnoDB", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + option.setTokens(List.of("ROW_FORMAT", "=", "DYNAMIC")); + option.setValue("'new'"); + assertEquals("'new'", option.getValue()); + assertNull(option.getName()); + assertEquals(TableOption.Kind.OTHER, option.getKind()); + option.withName("COMMENT").withUseEquals(true).withKind(TableOption.Kind.COMMENT); + assertEquals(List.of("COMMENT", "=", "'new'"), option.getTokens()); + assertThrows(UnsupportedOperationException.class, () -> option.getTokens().add("ignored")); + assertEquals("CREATE TABLE t (id INT) COMMENT = 'new'", create.toString()); + assertRoundTrip(create, Dialect.MYSQL); + option.setTokens(null); + assertEquals("COMMENT = 'new'", option.toString()); + option.setTokens(List.of("ENGINE", "CSV")); + option.setTokens(null); + assertEquals(TableOption.Kind.OTHER, option.getKind()); + assertNull(option.getName()); + assertNull(option.getValue()); + assertTrue(option.getTokens().isEmpty()); + } + + @Test + void unionReplacementStopsVisitingDetachedSourcesAndPreservesEmptyLists() + throws JSQLParserException { + CreateTable create = create("UNION=(old_source)", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + List
oldSources = option.getUnionTables(); + option.setTokens(List.of("UNION", "=", "(raw_source)")); + assertNull(option.getUnionTables()); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(create)); + oldSources.get(0).setName("detached"); + assertEquals("UNION = (raw_source)", option.toString()); + assertRoundTrip(create, Dialect.MYSQL); + option.setUnionTables(new ArrayList<>()); + assertEquals("()", option.getValue()); + assertEquals("UNION ()", option.toString()); + option.getUnionTables().add(new Table("new_source")); + assertEquals(Set.of("t", "new_source"), new TablesNamesFinder().getTables(create)); + assertRoundTrip(create, Dialect.MYSQL); + option.setUnionTables(null); + assertNull(option.getUnionTables()); + assertNull(option.getValue()); + assertEquals("UNION", option.toString()); + } + + @Test + void storageSelectionResetsScalarSpellingAndKeepsExpressionRendering() + throws JSQLParserException { + CreateTable create = create("ENGINE=InnoDB", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + LongValue fillFactor = new LongValue(70); + List parameters = new ArrayList<>( + List.of(new IndexOption("fillfactor", fillFactor, true))); + option.setStorageParameters(parameters); + assertSame(parameters, option.getStorageParameters()); + assertFalse(option.isUseEquals()); + assertEquals("WITH (fillfactor = 70)", option.toString()); + assertRoundTrip(create, Dialect.POSTGRESQL); + StringBuilder customized = new StringBuilder(); + option.appendTo(customized, expression -> customized.append("80")); + assertEquals("WITH (fillfactor = 80)", customized.toString()); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(option, visited::add, ignored -> { + }); + assertEquals(List.of(fillFactor), visited); + option.setTokens(List.of("WITH", "(fillfactor = 90)")); + assertNull(option.getStorageParameters()); + visited.clear(); + TableDefinitionTraversal.visit(option, visited::add, ignored -> { + }); + assertTrue(visited.isEmpty()); + assertRoundTrip(create, Dialect.POSTGRESQL); + option.setStorageParameters(null); + assertEquals(TableOption.Kind.STORAGE_PARAMETERS, option.getKind()); + assertNull(option.getValue()); + assertEquals("WITH", option.toString()); + } + + @Test + void foreignServerReplacementUpdatesCreateProjectionAndDoesNotReviveOldOptions() + throws JSQLParserException { + CreateTable create = (CreateTable) parse( + "CREATE FOREIGN TABLE t(id INT) SERVER old_srv OPTIONS(table_name 'old')", + Dialect.POSTGRESQL); + TableOption option = create.getTableOptions().get(0); + ForeignTableOptions original = option.getForeignTableOptions(); + option.setTokens(List.of("SERVER", "raw_srv")); + assertNull(create.getForeignTableOptions()); + assertNull(option.getForeignTableOptions()); + original.setServer("detached"); + assertRoundTrip(create, Dialect.POSTGRESQL); + ForeignTableOptions replacement = new ForeignTableOptions(); + replacement.setServer("new_srv"); + option.setForeignTableOptions(replacement); + assertSame(replacement, create.getForeignTableOptions()); + assertEquals(TableOption.Kind.FOREIGN_SERVER, option.getKind()); + assertEquals(List.of("SERVER", "new_srv"), option.getTokens()); + assertRoundTrip(create, Dialect.POSTGRESQL); + option.setValue("scalar_srv"); + assertNull(create.getForeignTableOptions()); + assertEquals("SERVER scalar_srv", option.toString()); + assertRoundTrip(create, Dialect.POSTGRESQL); + } + + @Test + void tablespaceStorageDoesNotLeakAcrossRawAndStructuredReplacements() + throws JSQLParserException { + CreateTable create = create("TABLESPACE=old_ts STORAGE DISK", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + option.setTokens(List.of("TABLESPACE", "raw_ts")); + assertNull(option.getTablespaceStorage()); + option.setTablespaceStorage(ColumnOption.Storage.MEMORY); + assertEquals(TableOption.Kind.TABLESPACE, option.getKind()); + assertEquals("TABLESPACE", option.getName()); + assertNull(option.getValue()); + option.setValue("new_ts"); + assertEquals(List.of("TABLESPACE", "new_ts", "STORAGE", "MEMORY"), option.getTokens()); + assertRoundTrip(create, Dialect.MYSQL); + option.setUnionTables(List.of(new Table("source"))); + assertNull(option.getTablespaceStorage()); + assertRoundTrip(create, Dialect.MYSQL); + assertThrows(IllegalArgumentException.class, + () -> option.setTablespaceStorage(ColumnOption.Storage.PLAIN)); + assertEquals("UNION (source)", option.toString()); + } + + private static CreateTable create(String options, Dialect dialect) throws JSQLParserException { + return (CreateTable) parse("CREATE TABLE t(id INT) " + options, dialect); + } + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static void assertRoundTrip(Statement statement, Dialect dialect) + throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString(), dialect).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/TablespaceStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/TablespaceStatementTest.java new file mode 100644 index 0000000000..6182bdef06 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/TablespaceStatementTest.java @@ -0,0 +1,368 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementFeatures; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.alter.AlterTablespace; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TablespaceStatementTest { + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + /** Parses with the dialect, compares both renderers with the input and reparses the output. */ + private static T assertRoundTrip(String sql, Dialect dialect, + Class type) throws JSQLParserException { + Statement statement = + assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withDialect(dialect)); + assertInstanceOf(type, statement); + Statement reparsed = parse(statement.toString(), dialect); + assertInstanceOf(type, reparsed); + assertEquals(statement.toString(), reparsed.toString()); + return type.cast(statement); + } + + private static List kinds(List options) { + return options.stream().map(TableOption::getKind).collect(Collectors.toList()); + } + + private static List names(List parameters) { + return parameters.stream().map(IndexOption::getName).collect(Collectors.toList()); + } + + @Test + void mysqlCreateTablespaceWithDataFileAndEngine() throws JSQLParserException { + CreateTablespace statement = assertRoundTrip( + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' ENGINE=InnoDB", Dialect.MYSQL, + CreateTablespace.class); + + assertEquals("ts1", statement.getName()); + assertFalse(statement.isUndo()); + assertEquals("ts1.ibd", statement.getDataFile().getValue()); + TableOption engine = statement.getOption(TableOption.Kind.ENGINE).orElseThrow(); + assertEquals("InnoDB", engine.getValue()); + assertTrue(engine.isUseEquals()); + assertNull(statement.getOwner()); + assertNull(statement.getLocation()); + assertNull(statement.getParameters()); + assertEquals("CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' ENGINE = InnoDB", + statement.toString()); + } + + @Test + void mysqlOptionsKeepSourceOrderAndReuseTableOptionModels() throws JSQLParserException { + CreateTablespace statement = (CreateTablespace) parse( + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' AUTOEXTEND_SIZE 4M, " + + "FILE_BLOCK_SIZE = 8192 ENCRYPTION 'Y', ENGINE_ATTRIBUTE = '{}' " + + "ENGINE InnoDB", + Dialect.MYSQL); + + assertEquals(List.of(TableOption.Kind.AUTOEXTEND_SIZE, TableOption.Kind.FILE_BLOCK_SIZE, + TableOption.Kind.ENCRYPTION, TableOption.Kind.ENGINE_ATTRIBUTE, + TableOption.Kind.ENGINE), kinds(statement.getOptions())); + assertEquals(List.of("4M", "8192", "'Y'", "'{}'", "InnoDB"), statement.getOptions() + .stream().map(TableOption::getValue).collect(Collectors.toList())); + assertEquals(List.of(false, true, false, true, false), statement.getOptions().stream() + .map(TableOption::isUseEquals).collect(Collectors.toList())); + // commas between options are optional and not retained + String expected = "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' AUTOEXTEND_SIZE 4M " + + "FILE_BLOCK_SIZE = 8192 ENCRYPTION 'Y' ENGINE_ATTRIBUTE = '{}' ENGINE InnoDB"; + assertEquals(expected, statement.toString()); + assertDeparse(statement, expected); + assertRoundTrip(expected, Dialect.MYSQL, CreateTablespace.class); + + CreateTable table = (CreateTable) parse( + "CREATE TABLE t (a INT) AUTOEXTEND_SIZE 4M ENCRYPTION 'Y' ENGINE InnoDB", + Dialect.MYSQL); + for (TableOption tableOption : table.getTableOptions()) { + TableOption tablespaceOption = + statement.getOption(tableOption.getKind()).orElseThrow(); + assertEquals(tableOption.getTokens(), tablespaceOption.getTokens()); + } + } + + @Test + void mysqlDataFileAndOptionsAreOptional() throws JSQLParserException { + CreateTablespace statement = + assertRoundTrip("CREATE TABLESPACE ts1", Dialect.MYSQL, CreateTablespace.class); + assertNull(statement.getDataFile()); + assertTrue(statement.getOptions().isEmpty()); + + statement = assertRoundTrip("CREATE TABLESPACE `ts 1` ENGINE = 'InnoDB'", Dialect.MYSQL, + CreateTablespace.class); + assertEquals("`ts 1`", statement.getName()); + assertNull(statement.getDataFile()); + assertEquals("'InnoDB'", statement.getOption(TableOption.Kind.ENGINE).orElseThrow() + .getValue()); + } + + @Test + void mysqlUndoTablespaces() throws JSQLParserException { + CreateTablespace create = assertRoundTrip( + "CREATE UNDO TABLESPACE u1 ADD DATAFILE 'u1.ibu' ENGINE = InnoDB", Dialect.MYSQL, + CreateTablespace.class); + assertTrue(create.isUndo()); + assertEquals("u1", create.getName()); + assertEquals("u1.ibu", create.getDataFile().getValue()); + assertEquals(List.of(TableOption.Kind.ENGINE), kinds(create.getOptions())); + + AlterTablespace inactive = assertRoundTrip("ALTER UNDO TABLESPACE u1 SET INACTIVE", + Dialect.MYSQL, AlterTablespace.class); + assertTrue(inactive.isUndo()); + assertEquals(AlterTablespace.Action.SET_STATE, inactive.getAction()); + assertEquals(AlterTablespace.State.INACTIVE, inactive.getState()); + assertTrue(inactive.getOptions().isEmpty()); + + AlterTablespace active = assertRoundTrip( + "ALTER UNDO TABLESPACE u1 SET ACTIVE ENGINE = InnoDB", Dialect.MYSQL, + AlterTablespace.class); + assertEquals(AlterTablespace.State.ACTIVE, active.getState()); + assertEquals(List.of(TableOption.Kind.ENGINE), kinds(active.getOptions())); + } + + @Test + void mysqlAlterTablespace() throws JSQLParserException { + AlterTablespace rename = assertRoundTrip("ALTER TABLESPACE ts1 RENAME TO ts2", + Dialect.MYSQL, AlterTablespace.class); + assertEquals("ts1", rename.getName()); + assertFalse(rename.isUndo()); + assertEquals(AlterTablespace.Action.RENAME, rename.getAction()); + assertEquals("ts2", rename.getNewName()); + + AlterTablespace options = (AlterTablespace) parse( + "ALTER TABLESPACE ts1 AUTOEXTEND_SIZE = 64M, ENCRYPTION = 'N' ENGINE = InnoDB " + + "ENGINE_ATTRIBUTE '{}'", + Dialect.MYSQL); + assertEquals(AlterTablespace.Action.OPTIONS, options.getAction()); + assertEquals(List.of(TableOption.Kind.AUTOEXTEND_SIZE, TableOption.Kind.ENCRYPTION, + TableOption.Kind.ENGINE, TableOption.Kind.ENGINE_ATTRIBUTE), + kinds(options.getOptions())); + assertEquals("'N'", options.getOption(TableOption.Kind.ENCRYPTION).orElseThrow() + .getValue()); + String expected = "ALTER TABLESPACE ts1 AUTOEXTEND_SIZE = 64M ENCRYPTION = 'N' " + + "ENGINE = InnoDB ENGINE_ATTRIBUTE '{}'"; + assertEquals(expected, options.toString()); + assertDeparse(options, expected); + assertRoundTrip(expected, Dialect.MYSQL, AlterTablespace.class); + } + + @Test + void postgresqlCreateTablespace() throws JSQLParserException { + CreateTablespace statement = assertRoundTrip( + "CREATE TABLESPACE fast OWNER u1 LOCATION '/ssd1/pg' " + + "WITH (random_page_cost = 1.1, effective_io_concurrency = 200)", + Dialect.POSTGRESQL, CreateTablespace.class); + + assertEquals("fast", statement.getName()); + assertEquals("u1", statement.getOwner()); + assertEquals("/ssd1/pg", statement.getLocation().getValue()); + assertEquals(List.of("random_page_cost", "effective_io_concurrency"), + names(statement.getParameters())); + assertEquals("1.1", statement.getParameters().get(0).getValue().toString()); + assertNull(statement.getDataFile()); + assertTrue(statement.getOptions().isEmpty()); + List values = new ArrayList<>(); + statement.visitExpressions(values::add); + assertEquals(2, values.size()); + + statement = assertRoundTrip("CREATE TABLESPACE fast LOCATION '/ssd1/pg'", + Dialect.POSTGRESQL, CreateTablespace.class); + assertNull(statement.getOwner()); + assertNull(statement.getParameters()); + + statement = assertRoundTrip("CREATE TABLESPACE \"Fast\" OWNER CURRENT_USER LOCATION ''", + Dialect.POSTGRESQL, CreateTablespace.class); + assertEquals("CURRENT_USER", statement.getOwner()); + assertEquals("", statement.getLocation().getValue()); + } + + @Test + void postgresqlAlterTablespace() throws JSQLParserException { + AlterTablespace rename = assertRoundTrip("ALTER TABLESPACE fast RENAME TO faster", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals(AlterTablespace.Action.RENAME, rename.getAction()); + assertEquals("faster", rename.getNewName()); + + AlterTablespace owner = assertRoundTrip("ALTER TABLESPACE fast OWNER TO SESSION_USER", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals(AlterTablespace.Action.OWNER, owner.getAction()); + assertEquals("SESSION_USER", owner.getOwner()); + + AlterTablespace set = assertRoundTrip( + "ALTER TABLESPACE fast SET (random_page_cost = 1.1, seq_page_cost = 2)", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals(AlterTablespace.Action.SET_PARAMETERS, set.getAction()); + assertEquals(List.of("random_page_cost", "seq_page_cost"), names(set.getParameters())); + List values = new ArrayList<>(); + set.visitExpressions(values::add); + assertEquals(2, values.size()); + + AlterTablespace reset = assertRoundTrip( + "ALTER TABLESPACE fast RESET (random_page_cost, seq_page_cost)", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals(AlterTablespace.Action.RESET_PARAMETERS, reset.getAction()); + assertEquals(List.of("random_page_cost", "seq_page_cost"), reset.getResetParameters()); + values.clear(); + reset.visitExpressions(values::add); + assertTrue(values.isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' ENGINE = InnoDB", + "CREATE UNDO TABLESPACE u1 ADD DATAFILE 'u1.ibu'", + "CREATE TABLESPACE fast OWNER u1 LOCATION '/ssd1/pg' WITH (seq_page_cost = 1)", + "ALTER TABLESPACE ts1 RENAME TO ts2", "ALTER TABLESPACE ts1 ENCRYPTION = 'Y'", + "ALTER UNDO TABLESPACE u1 SET INACTIVE", "ALTER TABLESPACE fast OWNER TO bob", + "ALTER TABLESPACE fast SET (seq_page_cost = 1)", + "ALTER TABLESPACE fast RESET (seq_page_cost)"}) + void statementsOnlyModifySchemaAndKeepScriptBoundaries(String sql) + throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + assertTrue(statement instanceof CreateTablespace || statement instanceof AlterTablespace); + StatementFeatures features = statement.getFeatures(); + assertEquals(EnumSet.of(StmtFeature.MODIFIES_SCHEMA), features.getCertain()); + assertTrue(features.getUncertain().isEmpty()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + + Statements script = CCJSqlParserUtil.parseStatements(sql + ";\n" + sql + "; SELECT 1"); + assertEquals(3, script.size()); + assertEquals(statement.getClass(), script.get(0).getClass()); + assertEquals(statement.getClass(), script.get(1).getClass()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE UNDO TABLESPACE u1", + "CREATE UNDO TABLESPACE u1 ADD DATAFILE 'u1.ibu' ENCRYPTION = 'Y'", + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' KEY_BLOCK_SIZE = 8", + "CREATE TABLESPACE ts1 AUTOEXTEND_SIZE = 4X", + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd', ENGINE = InnoDB", + "CREATE TABLESPACE ts1 ADD DATAFILE ts1", + "CREATE OR REPLACE TABLESPACE ts1", + "CREATE TABLESPACE fast OWNER u1", + "CREATE TABLESPACE fast LOCATION '/ssd1/pg' ENGINE = InnoDB", + "CREATE TABLESPACE fast ADD DATAFILE 'f.ibd' LOCATION '/ssd1/pg'", + "ALTER TABLESPACE ts1 FILE_BLOCK_SIZE = 8K", + "ALTER UNDO TABLESPACE u1 SET ACTIVE ENCRYPTION = 'Y'"}) + void rejectsFormsOutsideTheStatementDefinitions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @Test + void postgresqlParameterValuesRequireEquals() { + assertThrows(JSQLParserException.class, () -> parse( + "CREATE TABLESPACE fast LOCATION '/ssd1/pg' WITH (random_page_cost 1.1)", + Dialect.POSTGRESQL)); + assertThrows(JSQLParserException.class, + () -> parse("ALTER TABLESPACE fast SET (random_page_cost 1.1)", + Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLESPACE users ADD DATAFILE '/u01/users02.dbf' SIZE 100M AUTOEXTEND ON", + "ALTER TABLESPACE users OFFLINE", "ALTER TABLESPACE users READ ONLY", + "ALTER TABLESPACE users ENCRYPTION ONLINE USING 'AES256' ENCRYPT", + "ALTER TABLESPACE ts1 ADD DATAFILE 'ts1_2.dat' INITIAL_SIZE = 1M", + "ALTER TABLESPACE ts1 ENGINE = NDB WAIT", + "ALTER TABLESPACE ts1 RENAME TO ts2 ENGINE = InnoDB", + "ALTER UNDO TABLESPACE u1 RENAME TO u2"}) + void otherAlterTablespaceFormsRemainUnsupportedStatements(String sql) + throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertInstanceOf(UnsupportedStatement.class, statement); + assertEquals(sql, statement.toString()); + + Statements script = CCJSqlParserUtil.parseStatements( + sql + ";\nALTER TABLESPACE ts1 RENAME TO ts2;"); + assertInstanceOf(UnsupportedStatement.class, script.get(0)); + assertInstanceOf(AlterTablespace.class, script.get(1)); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE ALL IN TABLESPACE slow SET TABLESPACE fast", + "ALTER INDEX ALL IN TABLESPACE slow OWNED BY alice SET TABLESPACE fast NOWAIT", + "ALTER MATERIALIZED VIEW ALL IN TABLESPACE slow SET TABLESPACE fast"}) + void bulkTablespaceMovesRemainSeparate(String sql) throws JSQLParserException { + assertRoundTrip(sql, Dialect.POSTGRESQL, AlterTablespaceMove.class); + } + + @Test + void clauseWordsRemainIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (undo INT, location INT, datafile INT, " + + "inactive INT, file_block_size INT, owner INT)"); + CreateTablespace create = assertRoundTrip("CREATE TABLESPACE undo ADD DATAFILE 'u.ibd'", + Dialect.MYSQL, CreateTablespace.class); + assertEquals("undo", create.getName()); + assertFalse(create.isUndo()); + AlterTablespace alter = assertRoundTrip("ALTER TABLESPACE location RENAME TO owner", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals("location", alter.getName()); + assertEquals("owner", alter.getNewName()); + } + + @Test + void buildsAndDeparses() { + CreateTablespace create = new CreateTablespace(); + create.setName("fast"); + create.setOwner("bob"); + create.setLocation(new StringValue("/data")); + create.setParameters(List.of(new IndexOption("seq_page_cost", new LongValue(2), true))); + String expected = "CREATE TABLESPACE fast OWNER bob LOCATION '/data' " + + "WITH (seq_page_cost = 2)"; + assertEquals(expected, create.toString()); + assertDeparse(create, expected); + + AlterTablespace alter = new AlterTablespace(); + alter.setUndo(true); + alter.setName("u1"); + alter.setAction(AlterTablespace.Action.SET_STATE); + alter.setState(AlterTablespace.State.INACTIVE); + alter.addOptions(new TableOption(TableOption.Kind.ENGINE, "ENGINE", "InnoDB", true)); + expected = "ALTER UNDO TABLESPACE u1 SET INACTIVE ENGINE = InnoDB"; + assertEquals(expected, alter.toString()); + assertDeparse(alter, expected); + + alter.setAction(null); + assertThrows(IllegalStateException.class, alter::toString); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java b/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java new file mode 100644 index 0000000000..edeb7861cb --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java @@ -0,0 +1,266 @@ +/*- + * #%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.statement.create.database; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class CreateDatabaseTest { + + @Test + void testCreateDatabaseIssue2070() throws JSQLParserException { + String statement = "CREATE DATABASE USERS"; + assertSqlCanBeParsedAndDeparsed(statement); + assertDeparse(new CreateDatabase().withDatabaseName("USERS"), statement); + } + + @Test + void testCreateDatabaseIfNotExists() throws JSQLParserException { + String statement = "CREATE DATABASE IF NOT EXISTS mydb"; + assertSqlCanBeParsedAndDeparsed(statement); + assertDeparse(new CreateDatabase().withDatabaseName("mydb").withIfNotExists(true), + statement); + } + + @Test + void testCreateDatabaseQuotedName() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE DATABASE `my db`"); + assertSqlCanBeParsedAndDeparsed("CREATE DATABASE \"my db\""); + } + + @Test + void testCreateDatabaseWithOptions() throws JSQLParserException { + String statement = + "CREATE DATABASE IF NOT EXISTS mydb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"; + assertSqlCanBeParsedAndDeparsed(statement); + + Statement parsed = CCJSqlParserUtil.parse(statement); + CreateDatabase createDatabase = assertInstanceOf(CreateDatabase.class, parsed); + assertEquals("mydb", createDatabase.getDatabaseName()); + assertTrue(createDatabase.hasIfNotExists()); + assertEquals( + Arrays.asList("DEFAULT", "CHARACTER", "SET", "utf8mb4", "COLLATE", + "utf8mb4_unicode_ci"), + createDatabase.getDatabaseOptions()); + DatabaseOption characterSet = + createDatabase.getOption(DatabaseOption.Kind.CHARACTER_SET).orElseThrow(); + assertEquals("utf8mb4", characterSet.getValue()); + assertTrue(characterSet.isUseDefault()); + assertEquals("utf8mb4_unicode_ci", + createDatabase.getOption(DatabaseOption.Kind.COLLATE).orElseThrow().getValue()); + } + + @Test + void testCreateDatabaseAST() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("CREATE DATABASE USERS"); + CreateDatabase createDatabase = assertInstanceOf(CreateDatabase.class, statement); + assertEquals("USERS", createDatabase.getDatabaseName()); + assertFalse(createDatabase.hasIfNotExists()); + + statement = CCJSqlParserUtil.parse("CREATE DATABASE IF NOT EXISTS mydb"); + createDatabase = assertInstanceOf(CreateDatabase.class, statement); + assertEquals("mydb", createDatabase.getDatabaseName()); + assertTrue(createDatabase.hasIfNotExists()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE DATABASE mydb2", + "CREATE DATABASE db1 DEFAULT CHARSET = utf8mb4", + "CREATE DATABASE db1 CHAR SET utf8mb4 DEFAULT ENCRYPTION = 'N'", + "CREATE DATABASE IF NOT EXISTS db1 CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"}) + void testMySqlForms(String sql) throws JSQLParserException { + assertRoundTrip(sql, Dialect.MYSQL); + assertRoundTrip(sql, null); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE DATABASE mydb WITH OWNER = app ENCODING = 'UTF8' LC_COLLATE = 'C' " + + "LC_CTYPE = 'C' TEMPLATE = template0 CONNECTION LIMIT = 10", + "CREATE DATABASE mydb OWNER app STRATEGY = file_copy LOCALE 'C' " + + "BUILTIN_LOCALE = 'C.UTF-8' LOCALE_PROVIDER = builtin " + + "COLLATION_VERSION = '1' TABLESPACE = fast ALLOW_CONNECTIONS = true " + + "CONNECTION LIMIT = -1 IS_TEMPLATE false OID = 16384", + "CREATE DATABASE mydb WITH TEMPLATE DEFAULT ENCODING UTF8 LOCALE_PROVIDER icu " + + "ICU_LOCALE 'de-DE' ICU_RULES = '&a < b'"}) + void testPostgreSqlForms(String sql) throws JSQLParserException { + assertRoundTrip(sql, Dialect.POSTGRESQL); + assertRoundTrip(sql, null); + } + + @Test + void testPostgreSqlOptionsAreTyped() throws JSQLParserException { + CreateDatabase createDatabase = parse("CREATE DATABASE mydb WITH OWNER = app " + + "ENCODING = 'UTF8' TEMPLATE template0 CONNECTION LIMIT = -1 IS_TEMPLATE false", + Dialect.POSTGRESQL); + assertTrue(createDatabase.isUseWith()); + List options = createDatabase.getOptions(); + assertEquals(Arrays.asList(DatabaseOption.Kind.OWNER, DatabaseOption.Kind.ENCODING, + DatabaseOption.Kind.TEMPLATE, DatabaseOption.Kind.CONNECTION_LIMIT, + DatabaseOption.Kind.IS_TEMPLATE), + options.stream().map(DatabaseOption::getKind).collect( + java.util.stream.Collectors.toList())); + assertEquals("app", options.get(0).getValue()); + assertTrue(options.get(0).isUseEquals()); + assertEquals("'UTF8'", options.get(1).getValue()); + assertFalse(options.get(2).isUseEquals()); + assertEquals("CONNECTION LIMIT", options.get(3).getName()); + assertEquals("-1", options.get(3).getValue()); + assertEquals("false", options.get(4).getValue()); + + createDatabase.getOption(DatabaseOption.Kind.OWNER).orElseThrow().setValue("CURRENT_USER"); + options.remove(4); + assertEquals("CREATE DATABASE mydb WITH OWNER = CURRENT_USER ENCODING = 'UTF8' " + + "TEMPLATE template0 CONNECTION LIMIT = -1", createDatabase.toString()); + } + + @Test + void testCharsetSpellingIsTyped() throws JSQLParserException { + CreateDatabase createDatabase = + parse("CREATE DATABASE db1 DEFAULT CHARSET = utf8mb4", Dialect.MYSQL); + assertEquals(1, createDatabase.getOptions().size()); + DatabaseOption charset = createDatabase.getOptions().get(0); + assertEquals(DatabaseOption.Kind.CHARACTER_SET, charset.getKind()); + assertEquals("CHARSET", charset.getName()); + assertEquals("utf8mb4", charset.getValue()); + assertTrue(charset.isUseDefault()); + assertTrue(charset.isUseEquals()); + } + + @Test + void testNoEmptyRawOption() throws JSQLParserException { + CreateDatabase createDatabase = parse("CREATE DATABASE mydb2", null); + assertTrue(createDatabase.getOptions().isEmpty()); + assertNull(createDatabase.getDatabaseOptions()); + assertEquals("CREATE DATABASE mydb2", createDatabase.toString()); + + createDatabase = parse("CREATE DATABASE db1 CHARACTER SET utf8mb4", Dialect.MYSQL); + assertEquals(1, createDatabase.getOptions().size()); + assertEquals("CREATE DATABASE db1 CHARACTER SET utf8mb4", createDatabase.toString()); + } + + @Test + void testUnknownOptionTextStaysRaw() throws JSQLParserException { + CreateDatabase createDatabase = + assertInstanceOf(CreateDatabase.class, assertSqlCanBeParsedAndDeparsed( + "CREATE DATABASE mydb WITH OWNER app LOCATION 'x'")); + assertEquals(2, createDatabase.getOptions().size()); + assertEquals(DatabaseOption.Kind.OWNER, createDatabase.getOptions().get(0).getKind()); + DatabaseOption raw = createDatabase.getOptions().get(1); + assertEquals(DatabaseOption.Kind.OTHER, raw.getKind()); + assertEquals(Arrays.asList("LOCATION", "'x'"), raw.getTokens()); + + // WITH is kept with the raw text when no known option follows it. + createDatabase = assertInstanceOf(CreateDatabase.class, + assertSqlCanBeParsedAndDeparsed("CREATE DATABASE mydb WITH LOCATION 'x'")); + assertFalse(createDatabase.isUseWith()); + assertEquals(Arrays.asList("WITH", "LOCATION", "'x'"), + createDatabase.getOptions().get(0).getTokens()); + } + + @Test + void testLegacyDatabaseOptionTokens() throws JSQLParserException { + CreateDatabase createDatabase = parse( + "CREATE DATABASE mydb WITH OWNER = app CONNECTION LIMIT 5", Dialect.POSTGRESQL); + List tokens = createDatabase.getDatabaseOptions(); + assertEquals(Arrays.asList("WITH", "OWNER", "=", "app", "CONNECTION", "LIMIT", "5"), + tokens); + assertThrows(UnsupportedOperationException.class, () -> tokens.add("x")); + + createDatabase.getOptions().get(1).setValue("10"); + assertEquals("10", createDatabase.getDatabaseOptions().get(6)); + + createDatabase.addDatabaseOptions("IS_TEMPLATE", "true"); + assertEquals(DatabaseOption.Kind.OWNER, createDatabase.getOptions().get(0).getKind()); + assertEquals(DatabaseOption.Kind.OTHER, createDatabase.getOptions().get(2).getKind()); + assertEquals("CREATE DATABASE mydb WITH OWNER = app CONNECTION LIMIT 10 IS_TEMPLATE true", + createDatabase.toString()); + + List raw = new ArrayList<>(Arrays.asList("ENCODING", "'UTF8'")); + createDatabase.setDatabaseOptions(raw); + assertFalse(createDatabase.isUseWith()); + assertEquals(DatabaseOption.Kind.OTHER, createDatabase.getOptions().get(0).getKind()); + assertEquals("CREATE DATABASE mydb ENCODING 'UTF8'", createDatabase.toString()); + createDatabase.setDatabaseOptions(null); + assertTrue(createDatabase.getOptions().isEmpty()); + assertEquals("CREATE DATABASE mydb", createDatabase.toString()); + } + + @Test + void testOptionListOwnership() { + List options = new ArrayList<>(); + options.add(new DatabaseOption(DatabaseOption.Kind.OWNER, "OWNER", "app", false, true)); + CreateDatabase createDatabase = + new CreateDatabase().withDatabaseName("mydb").withUseWith(true) + .withOptions(options); + options.clear(); + assertEquals("CREATE DATABASE mydb WITH OWNER = app", createDatabase.toString()); + createDatabase.getOptions().add( + new DatabaseOption(DatabaseOption.Kind.IS_TEMPLATE, "IS_TEMPLATE", "true", false, + false)); + assertEquals("CREATE DATABASE mydb WITH OWNER = app IS_TEMPLATE true", + createDatabase.toString()); + createDatabase.setOptions(createDatabase.getOptions()); + assertEquals(2, createDatabase.getOptions().size()); + createDatabase.setOptions(null); + assertTrue(createDatabase.getOptions().isEmpty()); + } + + @Test + void testValidation() { + validateNoErrors("CREATE DATABASE mydb WITH OWNER = app", 1, DatabaseType.POSTGRESQL); + validateNoErrors("CREATE DATABASE db1 DEFAULT CHARSET = utf8mb4", 1, DatabaseType.MYSQL, + DatabaseType.MARIADB); + } + + /** Parses, deparses and reparses with the dialect, or without one if it is null. */ + private static void assertRoundTrip(String sql, Dialect dialect) throws JSQLParserException { + Consumer consumer = consumer(dialect); + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, consumer); + CreateDatabase createDatabase = assertInstanceOf(CreateDatabase.class, statement); + assertTrue(createDatabase.getOptions().stream() + .noneMatch(option -> option.getKind() == DatabaseOption.Kind.OTHER)); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString()); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", consumer).size()); + } + + private static CreateDatabase parse(String sql, Dialect dialect) throws JSQLParserException { + return (CreateDatabase) CCJSqlParserUtil.parse(sql, consumer(dialect)); + } + + private static Consumer consumer(Dialect dialect) { + return dialect == null ? parser -> { + } : parser -> parser.withDialect(dialect); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/event/EventStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/event/EventStatementTest.java new file mode 100644 index 0000000000..06f10422e4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/event/EventStatementTest.java @@ -0,0 +1,196 @@ +/*- + * #%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.statement.create.event; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.test.TestUtils.buildSqlString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Block; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.truncate.Truncate; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class EventStatementTest { + + @Test + void parsesCreateEventIntoStructuredProperties() throws JSQLParserException { + String sql = "CREATE EVENT hourly_rollup ON SCHEDULE EVERY 6 HOUR " + + "COMMENT 'Refresh aggregate' DO UPDATE app.metrics SET counter = counter + 1"; + CreateEvent event = + assertInstanceOf(CreateEvent.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertEquals("hourly_rollup", event.getEvent().getFullyQualifiedName()); + assertEquals(EventSchedule.Type.EVERY, event.getSchedule().getType()); + assertEquals(6, assertInstanceOf(LongValue.class, event.getSchedule().getInterval()) + .getValue()); + assertEquals("HOUR", event.getSchedule().getIntervalUnit()); + assertEquals("Refresh aggregate", event.getComment().getValue()); + assertInstanceOf(Update.class, event.getBody()); + } + + @Test + void parsesRecurringScheduleBounds() throws JSQLParserException { + String sql = "ALTER EVENT hourly_rollup ON SCHEDULE EVERY 12 HOUR " + + "STARTS CURRENT_TIMESTAMP + INTERVAL 4 HOUR " + + "ENDS CURRENT_TIMESTAMP + INTERVAL 2 DAY"; + AlterEvent event = + assertInstanceOf(AlterEvent.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertEquals(EventSchedule.Type.EVERY, event.getSchedule().getType()); + assertEquals("HOUR", event.getSchedule().getIntervalUnit()); + assertEquals("CURRENT_TIMESTAMP + INTERVAL 4 HOUR", + event.getSchedule().getStarts().toString()); + assertEquals("CURRENT_TIMESTAMP + INTERVAL 2 DAY", + event.getSchedule().getEnds().toString()); + } + + @Test + void parsesOneTimeScheduleAndReplacementBody() throws JSQLParserException { + String sql = "ALTER EVENT hourly_rollup ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY " + + "DO TRUNCATE TABLE app.metrics"; + AlterEvent event = + assertInstanceOf(AlterEvent.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertEquals(EventSchedule.Type.AT, event.getSchedule().getType()); + assertEquals("CURRENT_TIMESTAMP + INTERVAL 1 DAY", + event.getSchedule().getExecuteAt().toString()); + assertInstanceOf(Truncate.class, event.getBody()); + } + + @Test + void parsesStatusAndRenameOperations() throws JSQLParserException { + AlterEvent disabled = assertInstanceOf(AlterEvent.class, + assertSqlCanBeParsedAndDeparsed("ALTER EVENT hourly_rollup DISABLE")); + assertEquals(EventStatus.DISABLE, disabled.getStatus()); + assertNull(disabled.getRenameTo()); + + AlterEvent renamed = assertInstanceOf(AlterEvent.class, + assertSqlCanBeParsedAndDeparsed( + "ALTER EVENT hourly_rollup RENAME TO daily_rollup")); + assertEquals("daily_rollup", renamed.getRenameTo().getFullyQualifiedName()); + } + + @Test + void parsesCompletionPolicy() throws JSQLParserException { + String sql = "CREATE EVENT IF NOT EXISTS one_time_cleanup " + + "ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY " + + "ON COMPLETION NOT PRESERVE ENABLE DO TRUNCATE TABLE app.staging"; + CreateEvent event = + assertInstanceOf(CreateEvent.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertFalse(event.getOnCompletionPreserve()); + assertEquals(EventStatus.ENABLE, event.getStatus()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE DEFINER = `root`@`localhost` EVENT `e_daily` ON SCHEDULE EVERY 1 DAY " + + "STARTS CURRENT_TIMESTAMP + INTERVAL 6 HOUR ON COMPLETION NOT PRESERVE " + + "ENABLE COMMENT 'Saves total number of sessions then clears the table' " + + "DO BEGIN INSERT INTO site_activity.totals (time, total) " + + "SELECT CURRENT_TIMESTAMP, COUNT(*) FROM site_activity.sessions; " + + "DELETE FROM site_activity.sessions; END", + "CREATE DEFINER = 'admin'@'%' EVENT IF NOT EXISTS ev ON SCHEDULE EVERY 1 HOUR " + + "DO SELECT 1", + "CREATE DEFINER = CURRENT_USER() EVENT ev ON SCHEDULE AT CURRENT_TIMESTAMP " + + "DISABLE ON REPLICA DO SELECT 1", + "CREATE EVENT ev ON SCHEDULE EVERY 1 DAY DO BEGIN END", + "CREATE EVENT ev ON SCHEDULE EVERY 1 DAY " + + "DO BEGIN IF @a > 0 THEN DELETE FROM t1; END IF; END", + "ALTER DEFINER = CURRENT_USER EVENT ev ON COMPLETION PRESERVE RENAME TO db2.ev2 " + + "ENABLE COMMENT 'moved' DO SELECT 2", + "ALTER EVENT ev DISABLE ON REPLICA", + "ALTER EVENT ev DISABLE ON SLAVE", + "ALTER EVENT ev DO BEGIN DELETE FROM t1; DELETE FROM t2; END"}) + void parsesMySqlEventForms(String sql) throws JSQLParserException { + for (Consumer consumer : List.>of( + parser -> parser.withDialect(Dialect.MYSQL), parser -> { + })) { + // a statement block prints one statement per line + Statement event = assertSqlCanBeParsedAndDeparsed(sql, true, consumer); + assertInstanceOf(EventStatement.class, event); + Statements script = CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer); + assertEquals(2, script.size()); + assertEquals(buildSqlString(sql, true), + buildSqlString(script.get(0).toString(), true)); + } + } + + @Test + void parsesDefinerAsShowCreateEventPrintsIt() throws JSQLParserException { + CreateEvent event = assertInstanceOf(CreateEvent.class, CCJSqlParserUtil.parse( + "CREATE DEFINER=`root`@`localhost` EVENT `ev` ON SCHEDULE EVERY 1 DAY " + + "STARTS '2026-01-01 00:00:00' ON COMPLETION NOT PRESERVE ENABLE " + + "DO DELETE FROM t1")); + + assertEquals("root", event.getDefiner().getUser().getValue()); + assertEquals("localhost", event.getDefiner().getHost().getValue()); + assertInstanceOf(Delete.class, event.getBody()); + assertEquals("CREATE DEFINER = `root`@`localhost` EVENT `ev` ON SCHEDULE EVERY 1 DAY " + + "STARTS '2026-01-01 00:00:00' ON COMPLETION NOT PRESERVE ENABLE " + + "DO DELETE FROM t1", event.toString()); + + AlterEvent altered = assertInstanceOf(AlterEvent.class, CCJSqlParserUtil.parse( + "ALTER DEFINER = 'admin'@'%' EVENT ev ENABLE")); + assertEquals("admin", altered.getDefiner().getUser().getValue()); + assertEquals("%", altered.getDefiner().getHost().getValue()); + assertEquals(EventStatus.ENABLE, altered.getStatus()); + } + + @Test + void keepsStatementBlockBodiesStructured() throws JSQLParserException { + CreateEvent event = assertInstanceOf(CreateEvent.class, CCJSqlParserUtil.parse( + "CREATE EVENT ev ON SCHEDULE EVERY 1 DAY " + + "DO BEGIN DELETE FROM a; DELETE FROM b; END", + parser -> parser.withDialect(Dialect.MYSQL))); + + Block body = assertInstanceOf(Block.class, event.getBody()); + assertEquals(2, body.getStatements().size()); + assertInstanceOf(Delete.class, body.getStatements().get(0)); + assertEquals(Set.of("a", "b"), + new HashSet<>(new TablesNamesFinder<>().getTables((Statement) event))); + + AlterEvent altered = assertInstanceOf(AlterEvent.class, CCJSqlParserUtil.parse( + "ALTER EVENT ev DO BEGIN IF @a > 0 THEN DELETE FROM t1; END IF; END")); + assertTrue(altered.getBody() instanceof UnsupportedStatement); + } + + @Test + void parsesDisableOnReplica() throws JSQLParserException { + AlterEvent event = assertInstanceOf(AlterEvent.class, + CCJSqlParserUtil.parse("ALTER EVENT ev DISABLE ON REPLICA")); + assertEquals(EventStatus.DISABLE_ON_REPLICA, event.getStatus()); + + event = assertInstanceOf(AlterEvent.class, + CCJSqlParserUtil.parse("ALTER EVENT ev DISABLE ON SLAVE")); + assertEquals(EventStatus.DISABLE_ON_SLAVE, event.getStatus()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/schema/CreateSchemaTest.java b/src/test/java/net/sf/jsqlparser/statement/create/schema/CreateSchemaTest.java index 51aec7a84a..2bccbafb4c 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/schema/CreateSchemaTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/schema/CreateSchemaTest.java @@ -9,16 +9,52 @@ */ package net.sf.jsqlparser.statement.create.schema; -import net.sf.jsqlparser.JSQLParserException; import static net.sf.jsqlparser.test.TestUtils.assertDeparse; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.database.DatabaseOption; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.sequence.CreateSequence; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.statement.grant.Grant; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * * @author tw */ public class CreateSchemaTest { + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + private static CreateSchema roundTrip(String sql) throws JSQLParserException { + CreateSchema schema = assertInstanceOf(CreateSchema.class, + assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL)); + assertEquals(sql, CCJSqlParserUtil.parse(schema.toString(), POSTGRESQL).toString()); + return schema; + } @Test public void testSimpleCreateSchema() throws JSQLParserException { @@ -49,4 +85,118 @@ void testIfNotExistsIssue2061() throws JSQLParserException { String sqlStr = "CREATE SCHEMA IF NOT EXISTS sales_kpi"; assertSqlCanBeParsedAndDeparsed(sqlStr); } + + @Test + void testMySqlCreateSchemaOptions() throws JSQLParserException { + String sql = "CREATE SCHEMA IF NOT EXISTS schema_4 DEFAULT CHARACTER SET = utf8 " + + "COLLATE = utf8_general_ci DEFAULT ENCRYPTION = 'N'"; + CreateSchema schema = (CreateSchema) assertSqlCanBeParsedAndDeparsed(sql); + + DatabaseOption characterSet = schema + .getDatabaseOption(DatabaseOption.Kind.CHARACTER_SET).orElseThrow(); + assertEquals("utf8", characterSet.getValue()); + assertTrue(characterSet.isUseDefault()); + assertTrue(characterSet.isUseEquals()); + + DatabaseOption collate = + schema.getDatabaseOption(DatabaseOption.Kind.COLLATE).orElseThrow(); + assertEquals("utf8_general_ci", collate.getValue()); + + DatabaseOption encryption = + schema.getDatabaseOption(DatabaseOption.Kind.ENCRYPTION).orElseThrow(); + assertEquals("'N'", encryption.getValue()); + + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + } + + @Test + void testMySqlCreateSchemaCharset() throws JSQLParserException { + String sql = "CREATE SCHEMA db1 DEFAULT CHARSET = utf8mb4 DEFAULT ENCRYPTION = 'N'"; + for (Dialect dialect : new Dialect[] {Dialect.MYSQL, null}) { + CreateSchema schema = (CreateSchema) assertSqlCanBeParsedAndDeparsed(sql, false, + dialect == null ? parser -> { + } : parser -> parser.withDialect(dialect)); + DatabaseOption charset = + schema.getDatabaseOption(DatabaseOption.Kind.CHARACTER_SET).orElseThrow(); + assertEquals("CHARSET", charset.getName()); + assertEquals("utf8mb4", charset.getValue()); + assertTrue(charset.isUseDefault()); + } + assertSqlCanBeParsedAndDeparsed("CREATE SCHEMA db1 CHAR SET utf8mb4"); + } + + @Test + void keepsSchemaElementsAsWritten() throws JSQLParserException { + CreateSchema schema = roundTrip("CREATE SCHEMA hollywood " + + "CREATE TABLE films (title text, release date, awards text[]) " + + "CREATE VIEW winners AS SELECT title, release FROM films WHERE awards IS NOT NULL"); + + assertEquals("hollywood", schema.getSchemaName()); + assertEquals(2, schema.getStatements().size()); + CreateTable films = assertInstanceOf(CreateTable.class, schema.getStatements().get(0)); + assertEquals("films", films.getTable().getFullyQualifiedName()); + assertNull(films.getTable().getSchemaName()); + CreateView winners = assertInstanceOf(CreateView.class, schema.getStatements().get(1)); + assertEquals("winners", winners.getView().getFullyQualifiedName()); + assertThat(new TablesNamesFinder().getTables(schema)).containsExactlyInAnyOrder("films", + "winners"); + } + + @Test + void acceptsEveryPostgreSqlSchemaElement() throws JSQLParserException { + CreateSchema schema = roundTrip("CREATE SCHEMA app AUTHORIZATION app_owner " + + "CREATE TABLE t (id int) " + + "CREATE VIEW v AS SELECT id FROM t " + + "GRANT SELECT ON v TO PUBLIC " + + "CREATE UNIQUE INDEX i ON t (id) " + + "CREATE SEQUENCE s START WITH 10 " + + "CREATE TRIGGER trg BEFORE INSERT ON t FOR EACH ROW EXECUTE FUNCTION f() " + + "GRANT USAGE ON SEQUENCE s TO app_user"); + + assertEquals("app_owner", schema.getAuthorization()); + assertEquals(List.of(CreateTable.class, CreateView.class, Grant.class, CreateIndex.class, + CreateSequence.class, CreateTrigger.class, Grant.class), + schema.getStatements().stream().map(Object::getClass) + .collect(java.util.stream.Collectors.toList())); + assertThat(new TablesNamesFinder().getTables(schema)).contains("t", "v"); + assertThat(schema.getFeatures().getCertain()).containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertTrue(new Validation(List.of(PostgresqlVersion.V10), schema.toString()).validate() + .isEmpty()); + assertThat(new Validation(List.of(new FeaturesAllowed(Feature.createSchema)), + "CREATE SCHEMA app CREATE TABLE t (id int)").validate()).isNotEmpty(); + } + + @Test + void schemaElementsWithoutAName() throws JSQLParserException { + CreateSchema schema = roundTrip("CREATE SCHEMA AUTHORIZATION joe CREATE TABLE t (a int)"); + assertNull(schema.getSchemaName()); + assertEquals("t", ((CreateTable) schema.getStatements().get(0)).getTable().getName()); + } + + @Test + void grantRemainsAnAliasOutsideSchemaElements() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT a FROM t grant"); + assertEquals(2, CCJSqlParserUtil + .parseStatements("CREATE SCHEMA s;\nCREATE TABLE t (a int);").size()); + } + + @Test + void rendersSchemaPathAndElements() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE SCHEMA s PATH a, b"); + CreateSchema schema = new CreateSchema().withSchemaName("s"); + schema.addStatement(CCJSqlParserUtil.parse("CREATE TABLE t (a int)")); + assertDeparse(schema, "CREATE SCHEMA s CREATE TABLE t (a int)"); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE SCHEMA IF NOT EXISTS s CREATE TABLE t (a int)", + "CREATE SCHEMA s CREATE MATERIALIZED VIEW v AS SELECT 1", + "CREATE SCHEMA s CREATE FUNCTION f() RETURNS int AS 'SELECT 1' LANGUAGE sql", + "CREATE SCHEMA s CREATE SCHEMA t", + "CREATE SCHEMA s VIEW v AS SELECT 1", + "CREATE SCHEMA s CREATE TABLE t (a int) REVOKE SELECT ON t FROM u"}) + void rejectsOtherSchemaElements(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, POSTGRESQL)); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/table/CharacterSetShorthandTest.java b/src/test/java/net/sf/jsqlparser/statement/create/table/CharacterSetShorthandTest.java new file mode 100644 index 0000000000..e850426276 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/table/CharacterSetShorthandTest.java @@ -0,0 +1,123 @@ +/*- + * #%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.statement.create.table; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.CastExpression; +import net.sf.jsqlparser.expression.JsonFunction; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ColDataType.CharacterSetSyntax; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.*; + +class CharacterSetShorthandTest { + @ParameterizedTest + @CsvSource({"ASCII,latin1", "UNICODE,ucs2"}) + void shorthandsExposeCharacterSetAndRetainSpelling(String shorthand, String charset) + throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT CAST('ab' AS CHAR(10) " + shorthand + "), " + + "JSON_VALUE('{\"v\":\"ab\"}', '$.v' RETURNING CHAR(10) " + shorthand + + ")", + true, parser -> parser.withDialect(Dialect.MYSQL)); + ColDataType castType = + ((CastExpression) select.getSelectItem(0).getExpression()).getColDataType(); + ColDataType jsonType = + ((JsonFunction) select.getSelectItem(1).getExpression()).getReturningType(); + for (ColDataType type : new ColDataType[] {castType, jsonType}) { + assertEquals("CHAR", type.getBaseTypeName()); + assertEquals(charset, type.getCharacterSet()); + assertEquals(CharacterSetSyntax.valueOf(shorthand), type.getCharacterSetSyntax()); + } + TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false, + parser -> parser.withDialect(Dialect.MYSQL)); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE t (j JSON, g CHAR(10) AS (JSON_VALUE(j, '$.v' RETURNING CHAR(10) " + + shorthand + ")) STORED)", + true, parser -> parser.withDialect(Dialect.MYSQL)); + CreateTable create = (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE t (c CHAR(10) " + shorthand + ")", true, + parser -> parser.withDialect(Dialect.MYSQL)); + assertEquals(charset, + create.getColumnDefinitions().get(0).getColDataType().getCharacterSet()); + } + + @Test + void parsedReturningTypeCanBeChanged() throws JSQLParserException { + String sql = "SELECT JSON_VALUE('{\"v\":\"ab\"}', '$.v' RETURNING CHAR(10) ASCII)"; + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.MYSQL)); + ColDataType type = + ((JsonFunction) select.getSelectItem(0).getExpression()).getReturningType(); + type.setCharacterSetSyntax(CharacterSetSyntax.UNICODE); + TestUtils.assertStatementCanBeDeparsedAs(select, sql.replace("ASCII", "UNICODE"), true); + type.setCharacterSet("utf8mb4"); + TestUtils.assertStatementCanBeDeparsedAs(select, + sql.replace("ASCII", "CHARACTER SET utf8mb4"), true); + TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false, + parser -> parser.withDialect(Dialect.MYSQL)); + } + + @Test + void characterSetSyntaxCanBeConstructedChangedAndCleared() throws JSQLParserException { + ColDataType type = new ColDataType("CHAR").addArgumentsStringList("10") + .withCharacterSetSyntax(CharacterSetSyntax.ASCII); + assertEquals("latin1", type.getCharacterSet()); + assertEquals("CHAR (10) ASCII", type.toString()); + type.setCharacterSetSyntax(CharacterSetSyntax.UNICODE); + assertEquals("ucs2", type.getCharacterSet()); + assertEquals("CHAR (10) UNICODE", type.toString()); + type.setCharacterSet("utf8mb4"); + assertEquals(CharacterSetSyntax.CHARACTER_SET, type.getCharacterSetSyntax()); + assertEquals("CHAR (10) CHARACTER SET utf8mb4", type.toString()); + type.setUseCharsetKeyword(true); + assertTrue(type.isUseCharsetKeyword()); + type.setCharacterSet("latin1"); + assertEquals("CHAR (10) CHARSET latin1", type.toString()); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT CAST('ab' AS " + type + ")", true, + parser -> parser.withDialect(Dialect.MYSQL)); + type.setCharacterSetSyntax(CharacterSetSyntax.ASCII); + type.setUseCharsetKeyword(false); + assertEquals("CHAR (10) CHARACTER SET latin1", type.toString()); + type.setCharacterSetSyntax(CharacterSetSyntax.UNICODE); + type.setCharacterSet(null); + assertEquals("CHAR (10)", type.toString()); + } + + @Test + void syntaxParticipatesInEqualityAndHashCode() { + ColDataType ascii = + new ColDataType("CHAR").withCharacterSetSyntax(CharacterSetSyntax.ASCII); + ColDataType same = new ColDataType("char").withCharacterSetSyntax(CharacterSetSyntax.ASCII); + ColDataType explicit = new ColDataType("CHAR").withCharacterSet("latin1"); + assertEquals(ascii, same); + assertEquals(ascii.hashCode(), same.hashCode()); + assertNotEquals(ascii, explicit); + } + + @Test + void shorthandIsMySqlSpecificAndDoesNotReserveIdentifiers() throws JSQLParserException { + for (String shorthand : new String[] {"ASCII", "UNICODE"}) { + String sql = "SELECT CAST('ab' AS CHAR(10) " + shorthand + ")"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT " + shorthand + " FROM t", true, + parser -> parser.withDialect(Dialect.MYSQL)); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java b/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java index 5d282fb5ff..111b8b5715 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java @@ -11,12 +11,15 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.IntervalQualifier; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; class ColDataTypeTest { @Test @@ -97,4 +100,82 @@ void testCastAsIntervalWithQualifierRoundTrip() throws JSQLParserException { "SELECT CAST(col AS INTERVAL DAY TO SECOND)", true); assertSqlCanBeParsedAndDeparsed("SELECT CAST(col AS INTERVAL HOUR)", true); } + + @Test + void testStructuredPrecisionForKeywordTypes() throws JSQLParserException { + ColDataType varchar = parseColumnType("CREATE TABLE t (a VARCHAR(255))"); + assertEquals(255, varchar.getPrecision()); + assertNull(varchar.getScale()); + // the rendered string keeps its historical shape + assertEquals("VARCHAR (255)", varchar.getDataType()); + + ColDataType decimal = parseColumnType("CREATE TABLE t (a DECIMAL(10, 2))"); + assertEquals(10, decimal.getPrecision()); + assertEquals(2, decimal.getScale()); + assertEquals("DECIMAL (10, 2)", decimal.getDataType()); + + ColDataType max = parseColumnType("CREATE TABLE t (a VARCHAR(MAX))"); + assertEquals(Integer.MAX_VALUE, max.getPrecision()); + + ColDataType plain = parseColumnType("CREATE TABLE t (a INT)"); + assertNull(plain.getPrecision()); + assertNull(plain.getScale()); + } + + @Test + void testStructuredPrecisionForIdentifierTypes() throws JSQLParserException { + ColDataType mediumInt = parseColumnType("CREATE TABLE t (a mediumint(9))"); + assertEquals(9, mediumInt.getPrecision()); + assertNull(mediumInt.getScale()); + // the string arguments stay available as before + assertEquals(java.util.List.of("9"), mediumInt.getArgumentsStringList()); + + // non-numeric arguments are not numeric parameters + ColDataType enumType = parseColumnType("CREATE TABLE t (a ENUM('small', 'medium'))"); + assertNull(enumType.getPrecision()); + assertNull(enumType.getScale()); + } + + @Test + void testStructuredPrecisionForZonedTypes() throws JSQLParserException { + ColDataType zoned = parseColumnType("CREATE TABLE t (a TIMESTAMP(3) WITH TIME ZONE)"); + assertEquals(3, zoned.getPrecision()); + assertNull(zoned.getScale()); + // the token image keeps its historical shape + assertEquals("TIMESTAMP(3) WITH TIME ZONE", zoned.getDataType()); + + ColDataType unparameterized = + parseColumnType("CREATE TABLE t (a TIMESTAMP WITH TIME ZONE)"); + assertNull(unparameterized.getPrecision()); + } + + @Test + void testOracleNumberWithWildcardPrecision() throws JSQLParserException { + ColDataType number = + parseColumnType("CREATE TABLE t (a NUMBER(*, 0), b NUMBER(*), c NUMBER(*, 2))"); + assertEquals("NUMBER (*, 0)", number.getDataType()); + assertEquals("NUMBER", number.getBaseTypeName()); + assertNull(number.getPrecision()); + assertEquals(0, number.getScale()); + + assertSqlCanBeParsedAndDeparsed("SELECT CAST(1.5 AS NUMBER(*, 0)) FROM dual", true); + + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseColDataType("NUMBER(*, *)")); + } + + @Test + void testOracleRawType() throws JSQLParserException { + ColDataType raw = parseColumnType("CREATE TABLE t (id RAW(16), payload raw(2000))"); + assertEquals("RAW", raw.getDataType()); + assertEquals(16, raw.getPrecision()); + + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD payload RAW(2000)", true); + assertSqlCanBeParsedAndDeparsed("SELECT CAST(HEXTORAW('AB') AS RAW(16)) FROM dual", true); + } + + private ColDataType parseColumnType(String sqlStr) throws JSQLParserException { + CreateTable create = (CreateTable) assertSqlCanBeParsedAndDeparsed(sqlStr, true); + return create.getColumnDefinitions().get(0).getColDataType(); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/table/MySqlTypeCharacterSetTest.java b/src/test/java/net/sf/jsqlparser/statement/create/table/MySqlTypeCharacterSetTest.java new file mode 100644 index 0000000000..e562fddcc2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/table/MySqlTypeCharacterSetTest.java @@ -0,0 +1,83 @@ +/*- + * #%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.statement.create.table; + +import net.sf.jsqlparser.expression.CastExpression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class MySqlTypeCharacterSetTest { + private Statement parse(String sql) throws Exception { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT CAST('xx' AS CHAR(16) CHARSET BINARY)", + "SELECT CAST('xx' AS CHAR(16) CHARACTER SET 'utf8mb4')", + "SELECT CONVERT('xx' USING utf8mb4)", + "CREATE TABLE t (c VARCHAR(20) CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin')", + "CREATE TABLE t (c TEXT CHARSET utf8mb4)", + "CREATE TABLE t (c SET('a','b') CHARSET 'utf8mb4')", + "CREATE TABLE t (c BINARY(16) AS (CAST('xx' AS CHAR(16) CHARSET BINARY)))", + "ALTER TABLE t MODIFY c ENUM('a','b') CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci'", + "ALTER TABLE t ADD c CHAR(4) CHARSET utf8mb4, ADD d VARCHAR(10) CHARACTER SET utf8mb4" + }) + void sharesTypeSyntaxAcrossCastsAndColumnDefinitions(String sql) throws Exception { + Statement statement = parse(sql); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), parse(output.toString()).toString()); + } + + @Test + void exposesCharsetAndRetainsAbbreviationWhenMutated() throws Exception { + CreateTable table = (CreateTable) parse("CREATE TABLE t (c VARCHAR(20) CHARSET 'utf8mb4')"); + ColDataType type = table.getColumnDefinitions().get(0).getColDataType(); + assertEquals("'utf8mb4'", type.getCharacterSet()); + assertTrue(type.isUseCharsetKeyword()); + type.setCharacterSet("latin1"); + assertEquals("CREATE TABLE t (c VARCHAR (20) CHARSET latin1)", table.toString()); + type.setUseCharsetKeyword(false); + assertEquals("CREATE TABLE t (c VARCHAR (20) CHARACTER SET latin1)", table.toString()); + type.setCharacterSet(null); + assertEquals("CREATE TABLE t (c VARCHAR (20))", table.toString()); + } + + @Test + void preservesDefaultApiSpellingAndTypeEquality() throws Exception { + ColDataType legacy = new ColDataType("CHAR").withCharacterSet("binary"); + assertEquals("CHAR CHARACTER SET binary", legacy.toString()); + PlainSelect select = (PlainSelect) parse("SELECT CAST('a' AS CHAR CHARACTER SET binary)"); + ColDataType parsed = + ((CastExpression) select.getSelectItem(0).getExpression()).getColDataType(); + assertEquals(legacy, parsed); + assertEquals(legacy.hashCode(), parsed.hashCode()); + parsed.setUseCharsetKeyword(true); + assertNotEquals(legacy, parsed); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT CAST('x' AS CHAR CHARSET)", + "SELECT CAST('x' AS CHAR CHARACTER SET)", + "SELECT CAST('x' AS CHAR CHARSET = utf8mb4)"}) + void requiresCharsetName(String sql) { + assertThrows(Exception.class, () -> parse(sql)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/table/SqlServerXmlTypeTest.java b/src/test/java/net/sf/jsqlparser/statement/create/table/SqlServerXmlTypeTest.java new file mode 100644 index 0000000000..5c065ed802 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/table/SqlServerXmlTypeTest.java @@ -0,0 +1,112 @@ +/*- + * #%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.statement.create.table; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlServerXmlTypeTest { + @ParameterizedTest + @ValueSource(strings = { + "XML(CONTENT dbo.OrderSchema)", + "XML(DOCUMENT dbo.OrderSchema)", + "XML(dbo.OrderSchema)", + "XML(OrderSchema)", + "[xml](CONTENT [Person].[AdditionalContactInfoSchemaCollection])", + "\"xml\"(DOCUMENT \"Person\".\"Order Schema\")", + "XML(CONTENT)", + "XML(DOCUMENT.Collection)", + "XML([schema.with.dot].[collection.with.dot])" + }) + void roundTripTypedXml(String typeSql) throws JSQLParserException { + CreateTable table = (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE dbo.t (payload " + typeSql + " NULL)", true, + parser -> parser.withDialect(Dialect.SQLSERVER)); + ColDataType type = table.getColumnDefinitions().get(0).getColDataType(); + assertNotNull(type.getXmlTypeModifier()); + assertNull(type.getArgumentsStringList()); + assertNull(type.getPrecision()); + assertNull(type.getScale()); + assertEquals(List.of("dbo.t"), new TablesNamesFinder().getTableList(table)); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t ADD payload XML(CONTENT dbo.OrderSchema)", + "ALTER TABLE t ALTER COLUMN payload XML(DOCUMENT dbo.OrderSchema)", + "DECLARE @payload XML(CONTENT dbo.OrderSchema)", + "CREATE TABLE t (untyped XML, n DECIMAL(10, 2), s VARCHAR(20))" + }) + void reuseTypeParsingAcrossStatements(String sql) throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.SQLSERVER)); + } + + @Test + void reproduceIssue1567() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("CREATE TABLE [Person].[Person] (" + + "[BusinessEntityID] [int] NOT NULL, [PersonType] [nchar](2) NOT NULL, " + + "[NameStyle] [dbo].[NameStyle] NOT NULL, [Title] [nvarchar](8) NULL, " + + "[FirstName] [dbo].[Name] NOT NULL, [MiddleName] [dbo].[Name] NULL, " + + "[LastName] [dbo].[Name] NOT NULL, [Suffix] [nvarchar](10) NULL, " + + "[EmailPromotion] [int] NOT NULL, " + + "[AdditionalContactInfo] [xml](CONTENT [Person].[AdditionalContactInfoSchemaCollection]) NULL, " + + "[Demographics] [xml](CONTENT [Person].[IndividualSurveySchemaCollection]) NULL, " + + "[rowguid] [uniqueidentifier] ROWGUIDCOL NOT NULL, [ModifiedDate] [datetime] NOT NULL)", + true, parser -> parser.withDialect(Dialect.SQLSERVER)); + } + + @ParameterizedTest + @ValueSource(strings = {"XML()", "XML(10)", "XML(OTHER dbo.c)", + "XML(CONTENT dbo.c, dbo.d)", "XML(CONTENT db.dbo.c)", "XML(CONTENT dbo.c"}) + void rejectInvalidModifier(String typeSql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("CREATE TABLE t (x " + typeSql + ")", + parser -> parser.withDialect(Dialect.SQLSERVER) + .withUnsupportedStatements(false))); + } + + @Test + void structuredValuesAndEquality() { + XmlTypeModifier omitted = new XmlTypeModifier(null, List.of("dbo", "c")); + assertNull(omitted.getKind()); + assertEquals(XmlTypeModifier.Kind.CONTENT, omitted.getEffectiveKind()); + assertThrows(UnsupportedOperationException.class, + () -> omitted.getSchemaCollection().clear()); + ColDataType lower = new ColDataType("xml").withXmlTypeModifier(omitted); + ColDataType upper = new ColDataType("XML").withXmlTypeModifier( + new XmlTypeModifier(null, List.of("dbo", "c"))); + assertEquals(lower, upper); + assertEquals(lower.hashCode(), upper.hashCode()); + assertNotEquals(lower, new ColDataType("xml")); + assertNotEquals(omitted, + new XmlTypeModifier(XmlTypeModifier.Kind.CONTENT, List.of("dbo", "c"))); + assertNotEquals(omitted, new XmlTypeModifier(null, List.of("dbo", "other"))); + lower.setXmlTypeModifier(null); + assertEquals("xml", lower.toString()); + } + + @Test + void caseInsensitiveTypeHashAlsoHandlesUnicode() { + ColDataType dotted = new ColDataType("\u0130"); + ColDataType plain = new ColDataType("i"); + assertEquals(dotted, plain); + assertEquals(dotted.hashCode(), plain.hashCode()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/table/TypeMetadataTest.java b/src/test/java/net/sf/jsqlparser/statement/create/table/TypeMetadataTest.java new file mode 100644 index 0000000000..d5ec47ec26 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/table/TypeMetadataTest.java @@ -0,0 +1,206 @@ +/*- + * #%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.statement.create.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.CastExpression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class TypeMetadataTest { + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "MYSQL | DECIMAL(65, 30) | DECIMAL | 65 | 30", + "MYSQL | NUMERIC(1, 0) | NUMERIC | 1 | 0", + "MYSQL | VARCHAR(255) | VARCHAR | 255 |", + "MYSQL | NATIONAL VARCHAR(20) | NATIONAL VARCHAR | 20 |", + "MYSQL | mediumint(9) UNSIGNED | mediumint | 9 |", + "MYSQL | ENUM('a(b)', 'c') | ENUM | |", + "POSTGRESQL | NUMERIC(1000, 1000) | NUMERIC | 1000 | 1000", + "POSTGRESQL | NUMERIC(2, -1) | NUMERIC | 2 | -1", + "POSTGRESQL | NUMERIC(1000, -1000)[] | NUMERIC | 1000 | -1000", + "POSTGRESQL | pg_catalog.numeric(2, -3) | pg_catalog.numeric | 2 | -3", + "POSTGRESQL | \"numeric\"(2, -3) | \"numeric\" | 2 | -3", + "POSTGRESQL | NUMERIC(3, 5) | NUMERIC | 3 | 5", + "POSTGRESQL | DECIMAL(10) | DECIMAL | 10 |", + "POSTGRESQL | CHARACTER VARYING(255) | CHARACTER VARYING | 255 |", + "POSTGRESQL | TIMESTAMP(6) WITH TIME ZONE | TIMESTAMP WITH TIME ZONE | 6 |", + "POSTGRESQL | TIME(0) WITHOUT TIME ZONE | TIME WITHOUT TIME ZONE | 0 |", + "POSTGRESQL | NUMERIC(10, 2)[] | NUMERIC | 10 | 2", + "POSTGRESQL | INTERVAL(6) | INTERVAL | 6 |"}) + void exposesMetadataAcrossFragmentsDdlAndPostgreSqlCasts(Dialect dialect, String typeName, + String base, Integer precision, Integer scale) throws JSQLParserException { + ColDataType fragment = parseType(typeName, dialect); + assertMetadata(fragment, base, precision, scale); + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE type_case (v " + typeName + ")", p -> p.withDialect(dialect)); + assertMetadata(table.getColumnDefinitions().get(0).getColDataType(), base, precision, + scale); + assertMetadata(parseType(fragment.toString(), dialect), base, precision, scale); + if (dialect == Dialect.POSTGRESQL) { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT CAST(NULL AS " + typeName + ")", p -> p.withDialect(dialect)); + CastExpression cast = select.getSelectItem(0).getExpression(CastExpression.class); + assertMetadata(cast.getColDataType(), base, precision, scale); + } + } + + // MySQL BLOB/TEXT family boundaries and the signed-int boundary, verified on MySQL 8.4. + @ParameterizedTest + @ValueSource(strings = {"0", "1", "255", "256", "65535", "65536", "16777215", + "16777216", "2147483647", "2147483648", "4294967295"}) + void retainsLargeMySqlLengthsThroughBothRenderers(String length) throws JSQLParserException { + BigInteger expected = new BigInteger(length); + for (String name : List.of("BLOB", "TEXT")) { + ColDataType fragment = parseType(name + "(" + length + ")", Dialect.MYSQL); + assertEquals(name, fragment.getBaseTypeName()); + assertEquals(expected, fragment.getNumericPrecision()); + assertFalse(fragment.isMaxPrecision()); + assertFalse(fragment.toString().contains("MAX")); + assertEquals(expected.bitLength() < Integer.SIZE ? Integer.valueOf(length) : null, + fragment.getPrecision()); + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE type_case (v " + name + "(" + length + "))", + p -> p.withDialect(Dialect.MYSQL)); + StringBuilder visitor = new StringBuilder(); + table.accept(new StatementDeParser(visitor)); + for (String rendered : List.of(table.toString(), visitor.toString())) { + CreateTable reparsed = (CreateTable) CCJSqlParserUtil.parse(rendered, + p -> p.withDialect(Dialect.MYSQL)); + ColDataType type = reparsed.getColumnDefinitions().get(0).getColDataType(); + assertEquals(expected, type.getNumericPrecision()); + assertFalse(type.isMaxPrecision()); + } + } + } + + @Test + void distinguishesMaxKeywordFromItsLegacySentinelValue() throws JSQLParserException { + ColDataType max = parseType("VARCHAR(MAX)", Dialect.SQLSERVER); + assertTrue(max.isMaxPrecision()); + assertNull(max.getNumericPrecision()); + assertEquals(Integer.MAX_VALUE, max.getPrecision()); + assertEquals("VARCHAR (MAX)", max.getDataType()); + ColDataType literal = parseType("BLOB(2147483647)", Dialect.MYSQL); + assertFalse(literal.isMaxPrecision()); + assertEquals(BigInteger.valueOf(Integer.MAX_VALUE), literal.getNumericPrecision()); + assertEquals("BLOB (2147483647)", literal.getDataType()); + assertEquals("VARCHAR (MAX)", new ColDataType("VARCHAR", Integer.MAX_VALUE, -1).toString()); + assertNull(new ColDataType("VARCHAR", -1, -1).getNumericPrecision()); + } + + @Test + void preservesNumericFactoryCompatibilityAlongsideExactParameters() { + ColDataType negative = ColDataType.fromNumericParameters("numeric", 2, -1); + assertMetadata(negative, "numeric", 2, -1); + assertEquals("numeric (2, -1)", negative.toString()); + + ColDataType max = ColDataType.fromNumericParameters("VARCHAR", Integer.MAX_VALUE, null); + assertTrue(max.isMaxPrecision()); + assertNull(max.getNumericPrecision()); + assertEquals("VARCHAR (MAX)", max.toString()); + + ColDataType numeric = ColDataType.fromTypeParameters("BLOB", "2147483647", null); + assertFalse(numeric.isMaxPrecision()); + assertEquals(BigInteger.valueOf(Integer.MAX_VALUE), numeric.getNumericPrecision()); + assertEquals("BLOB (2147483647)", numeric.toString()); + + ColDataType omitted = ColDataType.fromNumericParameters("numeric", null, null); + assertMetadata(omitted, "numeric", null, null); + assertEquals("numeric", omitted.toString()); + } + + @Test + void metadataSettersDoNotLeaveStaleLargeOrMaxValues() throws JSQLParserException { + ColDataType type = parseType("BLOB(4294967295)", Dialect.MYSQL); + type.setPrecision(5); + assertEquals(BigInteger.valueOf(5), type.getNumericPrecision()); + type.setPrecision(null); + assertNull(type.getNumericPrecision()); + type = parseType("VARCHAR(MAX)", Dialect.SQLSERVER); + type.setNumericPrecision(BigInteger.valueOf(2147483648L)); + assertFalse(type.isMaxPrecision()); + assertNull(type.getPrecision()); + assertEquals(BigInteger.valueOf(2147483648L), type.getNumericPrecision()); + } + + @ParameterizedTest + @ValueSource(strings = {"\"odd(type)\"", "public.\"odd(type)\"", "\"a\"\"(b)\""}) + void retainsQuotedParenthesesAndQualification(String name) throws JSQLParserException { + assertEquals(name, parseType(name, Dialect.POSTGRESQL).getBaseTypeName()); + ColDataType manual = new ColDataType(name, 10, 2); + assertEquals(name, manual.getBaseTypeName()); + assertEquals(name + " (10, 2)", manual.getDataType()); + } + + @Test + void baseNameFollowsLegacyTypeEditsWithoutStaleCache() { + ColDataType type = new ColDataType("DECIMAL", 10, 2); + type.setDataType("VARCHAR (20)"); + assertEquals("VARCHAR", type.getBaseTypeName()); + assertEquals("VARCHAR (20)", type.getDataType()); + type.setDataType(List.of("app", "\"kind(1)\"")); + assertEquals("app.\"kind(1)\"", type.getBaseTypeName()); + type.setDataType((String) null); + assertNull(type.getBaseTypeName()); + } + + @ParameterizedTest + @ValueSource(strings = {"BLOB(-)", "BLOB(1.5)", "BLOB(4294967295", "DECIMAL(10,)"}) + void rejectsMalformedNumericParameters(String type) { + assertThrows(JSQLParserException.class, () -> parseType(type, Dialect.MYSQL)); + } + + @Test + void leavesDatabaseSpecificRangeValidationToTheDatabase() throws JSQLParserException { + // Syntactically numeric, but MySQL rejects this BLOB length as out of range. + String length = "18446744073709551616"; + assertEquals(new BigInteger(length), + parseType("BLOB(" + length + ")", Dialect.MYSQL).getNumericPrecision()); + } + + @Test + void preservesSignedGenericArgumentsWithoutTreatingThemAsLengths() throws JSQLParserException { + // The shared type-argument grammar accepts signed integers; MySQL rejects this length. + ColDataType type = parseType("BLOB(-1)", Dialect.MYSQL); + assertEquals(List.of("-1"), type.getArgumentsStringList()); + assertMetadata(type, "BLOB", null, null); + assertEquals(List.of("-1"), + parseType(type.toString(), Dialect.MYSQL).getArgumentsStringList()); + } + + private static ColDataType parseType(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parseColDataType(sql, p -> p.withDialect(dialect)); + } + + private static void assertMetadata(ColDataType type, String base, Integer precision, + Integer scale) { + assertEquals(base, type.getBaseTypeName()); + assertEquals(precision, type.getPrecision()); + assertEquals(precision == null ? null : BigInteger.valueOf(precision), + type.getNumericPrecision()); + assertEquals(scale, type.getScale()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java b/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java new file mode 100644 index 0000000000..ce5e896761 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java @@ -0,0 +1,210 @@ +/*- + * #%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.statement.create.trigger; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Block; +import net.sf.jsqlparser.statement.SetStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class CreateTriggerTest { + + @Test + void parsesTriggerDefinitionAndSetBody() throws JSQLParserException { + String sql = "CREATE TRIGGER normalize_name BEFORE INSERT ON customer " + + "FOR EACH ROW SET NEW.name = UPPER(NEW.name)"; + CreateTrigger trigger = + assertInstanceOf(CreateTrigger.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertEquals("normalize_name", trigger.getTrigger().getFullyQualifiedName()); + assertEquals(CreateTrigger.Timing.BEFORE, trigger.getTiming()); + assertEquals(CreateTrigger.Event.INSERT, trigger.getEvent()); + assertEquals("customer", trigger.getTable().getFullyQualifiedName()); + assertNull(trigger.getOrder()); + SetStatement body = assertInstanceOf(SetStatement.class, trigger.getBody()); + assertEquals("NEW.name", body.getName().toString()); + assertEquals("UPPER(NEW.name)", body.getExpressions().get(0).toString()); + } + + @Test + void parsesAfterTriggerWithInsertBody() throws JSQLParserException { + String sql = "CREATE TRIGGER record_delete AFTER DELETE ON customer FOR EACH ROW " + + "INSERT INTO audit_log (customer_id) VALUES (OLD.id)"; + CreateTrigger trigger = + assertInstanceOf(CreateTrigger.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertEquals(CreateTrigger.Timing.AFTER, trigger.getTiming()); + assertEquals(CreateTrigger.Event.DELETE, trigger.getEvent()); + assertInstanceOf(Insert.class, trigger.getBody()); + } + + @Test + void parsesDefinerWithoutReparsingAccountValues() throws JSQLParserException { + String sql = "CREATE DEFINER = 'automation'@'localhost' TRIGGER normalize_name " + + "BEFORE UPDATE ON customer FOR EACH ROW SET NEW.name = UPPER(NEW.name)"; + CreateTrigger trigger = + assertInstanceOf(CreateTrigger.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertEquals("automation", trigger.getDefiner().getUser().getValue()); + assertEquals("localhost", trigger.getDefiner().getHost().getValue()); + } + + @Test + void parsesTriggerOrdering() throws JSQLParserException { + String sql = "CREATE TRIGGER normalize_name_second BEFORE INSERT ON customer " + + "FOR EACH ROW FOLLOWS normalize_name SET NEW.name = TRIM(NEW.name)"; + CreateTrigger trigger = + assertInstanceOf(CreateTrigger.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertEquals(CreateTrigger.Order.FOLLOWS, trigger.getOrder()); + assertEquals("normalize_name", trigger.getOtherTrigger().getFullyQualifiedName()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE DEFINER = `root`@`localhost` TRIGGER IF NOT EXISTS trg2 AFTER UPDATE ON t1 " + + "FOR EACH ROW FOLLOWS trg INSERT INTO audit (id) VALUES (NEW.id)", + "CREATE TRIGGER IF NOT EXISTS trg2 AFTER UPDATE ON t1 FOR EACH ROW " + + "INSERT INTO audit (id) VALUES (NEW.id)", + "CREATE OR REPLACE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW SET NEW.a = 1", + "CREATE OR REPLACE DEFINER = CURRENT_USER TRIGGER trg BEFORE INSERT ON t1 " + + "FOR EACH ROW SET NEW.a = 1", + "CREATE TRIGGER trg3 BEFORE UPDATE ON t1 FOR EACH ROW " + + "BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END", + "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW lbl: BEGIN " + + "DECLARE x INT DEFAULT 0; WHILE x < 3 DO SET x = x + 1; END WHILE; " + + "LEAVE lbl; END lbl", + "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW " + + "IF NEW.a IS NULL THEN SET NEW.a = 'a.b'; END IF", + "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW CASE WHEN NEW.a > 0 THEN " + + "SET NEW.b = 1; ELSE SET NEW.b = CASE NEW.c WHEN 1 THEN 2 END; END CASE", + "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW BEGIN END"}) + void parsesMySqlTriggerForms(String sql) throws JSQLParserException { + for (Consumer consumer : List.>of( + parser -> parser.withDialect(Dialect.MYSQL), + parser -> parser.withDialect(Dialect.MARIADB), parser -> { + })) { + CreateTrigger trigger = assertInstanceOf(CreateTrigger.class, + assertSqlCanBeParsedAndDeparsed(sql, false, consumer)); + StringBuilder deparsed = new StringBuilder(); + trigger.accept(new StatementDeParser(deparsed)); + assertEquals(sql, deparsed.toString()); + assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString()); + Statements script = CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer); + assertEquals(2, script.size()); + assertEquals(sql, script.get(0).toString()); + } + } + + @Test + void parsesIfNotExistsAndOrReplace() throws JSQLParserException { + CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse( + "CREATE DEFINER = CURRENT_USER TRIGGER IF NOT EXISTS trg AFTER UPDATE ON t1 " + + "FOR EACH ROW INSERT INTO audit (id) VALUES (NEW.id)"); + assertTrue(trigger.isIfNotExists()); + assertFalse(trigger.isOrReplace()); + assertEquals("trg", trigger.getTrigger().getFullyQualifiedName()); + assertEquals("CURRENT_USER", trigger.getDefiner().getUser().getValue()); + + trigger.setIfNotExists(false); + trigger.setOrReplace(true); + assertEquals("CREATE OR REPLACE DEFINER = CURRENT_USER TRIGGER trg AFTER UPDATE ON t1 " + + "FOR EACH ROW INSERT INTO audit (id) VALUES (NEW.id)", trigger.toString()); + + CreateTrigger replaced = (CreateTrigger) CCJSqlParserUtil.parse( + "CREATE OR REPLACE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW SET NEW.a = 1", + parser -> parser.withDialect(Dialect.MARIADB)); + assertTrue(replaced.isOrReplace()); + assertFalse(replaced.isIfNotExists()); + + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "CREATE OR REPLACE TRIGGER IF NOT EXISTS trg BEFORE INSERT ON t1 " + + "FOR EACH ROW SET NEW.a = 1")); + } + + @Test + void keepsStatementBlocksStructured() throws JSQLParserException { + String sql = "CREATE TRIGGER trg AFTER INSERT ON t1 FOR EACH ROW BEGIN " + + "INSERT INTO audit (id) VALUES (NEW.id); " + + "UPDATE counters SET n = CASE WHEN NEW.a > 0 THEN n + 1 ELSE n END; " + + "DELETE FROM pending WHERE id = NEW.id; END;"; + CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.MYSQL)); + Block body = assertInstanceOf(Block.class, trigger.getBody()); + assertEquals(3, body.getStatements().size()); + assertInstanceOf(Insert.class, body.getStatements().get(0)); + // The semicolon after END ends the CREATE TRIGGER statement. + assertFalse(body.hasSemicolonAfterEnd()); + assertEquals(Set.of("t1", "audit", "counters", "pending"), + new HashSet<>(new TablesNamesFinder().getTables((Statement) trigger))); + } + + @Test + void keepsProceduralBodiesAsTokens() throws JSQLParserException { + CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse( + "CREATE TRIGGER trg3 BEFORE UPDATE ON t1 FOR EACH ROW " + + "BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END", + parser -> parser.withDialect(Dialect.MYSQL)); + UnsupportedStatement body = assertInstanceOf(UnsupportedStatement.class, trigger.getBody()); + assertEquals("BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END", body.toString()); + assertEquals(Set.of("t1"), + new HashSet<>(new TablesNamesFinder().getTables((Statement) trigger))); + } + + @Test + void splitsScriptsAfterCompoundBodies() throws JSQLParserException { + String script = "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW " + + "BEGIN SET NEW.a = 1; SET NEW.b = 2; END;\n" + + "CREATE TABLE b (y INT);\n" + + "CREATE TRIGGER trg3 BEFORE UPDATE ON t1 FOR EACH ROW " + + "BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END;\n" + + "CREATE TABLE c (z INT);"; + Statements statements = CCJSqlParserUtil.parseStatements(script, + parser -> parser.withDialect(Dialect.MYSQL)); + assertEquals(4, statements.size()); + assertInstanceOf(Block.class, ((CreateTrigger) statements.get(0)).getBody()); + assertInstanceOf(CreateTable.class, statements.get(1)); + assertInstanceOf(UnsupportedStatement.class, ((CreateTrigger) statements.get(2)).getBody()); + assertInstanceOf(CreateTable.class, statements.get(3)); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW BEGIN IF NEW.a < 0 THEN SET NEW.a = 0;", + "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW BEGIN SET NEW.a = 1"}) + void rejectsUnterminatedCompoundBodies(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/user/CreateUserTest.java b/src/test/java/net/sf/jsqlparser/statement/create/user/CreateUserTest.java new file mode 100644 index 0000000000..c97fe12eb7 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/user/CreateUserTest.java @@ -0,0 +1,349 @@ +/*- + * #%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.statement.create.user; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.HexValue; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.user.PasswordLockOption.Kind; +import net.sf.jsqlparser.statement.create.user.PasswordLockOption.Setting; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class CreateUserTest { + + /** Parses with the dialect, checks both renderers, then reparses the rendered SQL. */ + static T roundTrip(Class type, String sql, Dialect dialect) + throws JSQLParserException { + T statement = assertInstanceOf(type, assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(dialect))); + Statement reparsed = + CCJSqlParserUtil.parse(statement.toString(), parser -> parser.withDialect(dialect)); + assertInstanceOf(type, reparsed); + assertEquals(sql, reparsed.toString()); + return statement; + } + + private static List names(List accounts) { + return accounts.stream().map(UserAccount::toString).collect(Collectors.toList()); + } + + @Test + void parsesAccountAndPasswordAsStructuredValues() throws JSQLParserException { + String sql = "CREATE USER IF NOT EXISTS 'replicator' IDENTIFIED BY 'replpass'"; + CreateUser createUser = + assertInstanceOf(CreateUser.class, CCJSqlParserUtil.parse(sql)); + + assertTrue(createUser.isIfNotExists()); + assertEquals(1, createUser.getAccounts().size()); + UserAccount account = createUser.getAccounts().get(0); + assertEquals("replicator", account.getUser().getValue()); + assertNull(account.getHost()); + assertEquals(UserAuthentication.Mode.BY_PASSWORD, account.getAuthentication().getMode()); + assertEquals("replpass", account.getAuthentication().getCredential().getValue()); + assertSqlCanBeParsedAndDeparsed(sql); + } + + @Test + void parsesHostAndOptionalAuthentication() throws JSQLParserException { + String sql = "CREATE USER IF NOT EXISTS 'snapshot'@'%'"; + CreateUser createUser = + assertInstanceOf(CreateUser.class, assertSqlCanBeParsedAndDeparsed(sql)); + + UserAccount account = createUser.getAccounts().get(0); + assertEquals("snapshot", account.getUser().getValue()); + assertEquals("%", account.getHost().getValue()); + assertNull(account.getAuthentication()); + } + + @Test + void parsesMultipleAccountsAndAuthenticationPlugin() throws JSQLParserException { + String sql = "CREATE USER alice@localhost IDENTIFIED WITH caching_sha2_password " + + "BY 'secret', 'service'@'10.%' IDENTIFIED BY RANDOM PASSWORD"; + CreateUser createUser = + assertInstanceOf(CreateUser.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertFalse(createUser.isIfNotExists()); + assertEquals(2, createUser.getAccounts().size()); + UserAuthentication pluginAuthentication = + createUser.getAccounts().get(0).getAuthentication(); + assertEquals(UserAuthentication.Mode.WITH_PLUGIN_BY_PASSWORD, + pluginAuthentication.getMode()); + assertEquals("caching_sha2_password", pluginAuthentication.getPlugin()); + assertEquals("secret", pluginAuthentication.getCredential().getValue()); + assertEquals(UserAuthentication.Mode.BY_RANDOM_PASSWORD, + createUser.getAccounts().get(1).getAuthentication().getMode()); + } + + @Test + void parsesQuotedPluginAndHashAsShowCreateUserPrintsThem() throws JSQLParserException { + String sql = "CREATE USER `app`@`%` IDENTIFIED WITH 'caching_sha2_password' " + + "AS '$A$005$hash' REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK"; + UserAuthentication authentication = + roundTrip(CreateUser.class, sql, Dialect.MYSQL).getAccounts().get(0) + .getAuthentication(); + + assertEquals(UserAuthentication.Mode.WITH_PLUGIN_AS_STRING, authentication.getMode()); + assertEquals("caching_sha2_password", authentication.getPlugin()); + assertEquals("'", authentication.getPluginName().getQuoteStr()); + assertEquals("$A$005$hash", authentication.getCredential().getValue()); + assertSqlCanBeParsedAndDeparsed(sql); + } + + @Test + void parsesHexadecimalHash() throws JSQLParserException { + String sql = "CREATE USER 'app'@'%' IDENTIFIED WITH 'caching_sha2_password' " + + "AS 0x244124303035240A"; + UserAuthentication authentication = + roundTrip(CreateUser.class, sql, Dialect.MYSQL).getAccounts().get(0) + .getAuthentication(); + + assertInstanceOf(HexValue.class, authentication.getCredentialValue()); + assertNull(authentication.getCredential()); + assertEquals("0x244124303035240A", authentication.getCredentialValue().toString()); + roundTrip(CreateUser.class, "CREATE USER app IDENTIFIED WITH mysql_native_password " + + "AS X'2A3132'", Dialect.MYSQL); + } + + @ParameterizedTest + @ValueSource(strings = {"`caching_sha2_password`", "\"caching_sha2_password\"", + "caching_sha2_password"}) + void keepsThePluginQuoteSeparately(String plugin) throws JSQLParserException { + UserAuthentication authentication = roundTrip(CreateUser.class, + "CREATE USER app IDENTIFIED WITH " + plugin, Dialect.MYSQL).getAccounts().get(0) + .getAuthentication(); + + assertEquals("caching_sha2_password", authentication.getPlugin()); + assertEquals(plugin, authentication.getPluginName().toString()); + } + + @Test + void buildsPluginAuthenticationWithHash() { + UserAuthentication authentication = new UserAuthentication() + .withMode(UserAuthentication.Mode.WITH_PLUGIN_AS_STRING) + .withPluginName(new StringValue("'caching_sha2_password'")) + .withCredentialValue(new HexValue("0x2A")); + + assertEquals("IDENTIFIED WITH 'caching_sha2_password' AS 0x2A", authentication.toString()); + assertThrows(IllegalArgumentException.class, + () -> authentication.setCredentialValue(new LongValue(1))); + authentication.setPlugin("mysql_native_password"); + assertEquals("IDENTIFIED WITH mysql_native_password AS 0x2A", authentication.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE USER app IDENTIFIED BY 0x2A", + "CREATE USER app IDENTIFIED WITH caching_sha2_password BY 0x2A"}) + void acceptsHexadecimalOnlyAfterAs(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } + + @Test + void parsesSharedOptionsInClauseOrder() throws JSQLParserException { + String sql = "CREATE USER IF NOT EXISTS 'app'@'10.0.0.%' IDENTIFIED WITH " + + "caching_sha2_password BY 'secret' DEFAULT ROLE 'reader' REQUIRE SSL " + + "WITH MAX_QUERIES_PER_HOUR 100 PASSWORD EXPIRE INTERVAL 90 DAY " + + "FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 2 ACCOUNT LOCK COMMENT 'app user'"; + CreateUser createUser = roundTrip(CreateUser.class, sql, Dialect.MYSQL); + + UserOptions options = createUser.getOptions(); + assertEquals(RoleSelection.Kind.ROLES, options.getDefaultRoles().getKind()); + assertEquals(List.of("'reader'"), names(options.getDefaultRoles().getRoles())); + assertEquals(TlsOption.Kind.SSL, options.getTlsOptions().get(0).getKind()); + ResourceLimit limit = options.getResourceLimits().get(0); + assertEquals(ResourceLimit.Kind.MAX_QUERIES_PER_HOUR, limit.getKind()); + assertEquals(100, limit.getValue()); + assertThat(options.getPasswordLockOptions()).extracting(PasswordLockOption::getKind) + .containsExactly(Kind.PASSWORD_EXPIRE, Kind.FAILED_LOGIN_ATTEMPTS, + Kind.PASSWORD_LOCK_TIME, Kind.ACCOUNT_LOCK); + assertThat(options.getPasswordLockOptions()).extracting(PasswordLockOption::getValue) + .containsExactly(90L, 3L, 2L, null); + assertEquals("app user", options.getComment().getValue()); + assertNull(options.getAttribute()); + } + + @Test + void parsesPasswordSettingsAndRepeatedOptions() throws JSQLParserException { + String sql = "CREATE USER app PASSWORD EXPIRE NEVER PASSWORD EXPIRE DEFAULT " + + "PASSWORD EXPIRE PASSWORD HISTORY DEFAULT PASSWORD HISTORY 5 " + + "PASSWORD REUSE INTERVAL DEFAULT PASSWORD REUSE INTERVAL 365 DAY " + + "PASSWORD REQUIRE CURRENT PASSWORD REQUIRE CURRENT DEFAULT " + + "PASSWORD REQUIRE CURRENT OPTIONAL PASSWORD_LOCK_TIME UNBOUNDED " + + "ACCOUNT UNLOCK ACCOUNT LOCK"; + List options = roundTrip(CreateUser.class, sql, Dialect.MYSQL) + .getOptions().getPasswordLockOptions(); + + assertThat(options).extracting(PasswordLockOption::getKind).containsExactly( + Kind.PASSWORD_EXPIRE, Kind.PASSWORD_EXPIRE, Kind.PASSWORD_EXPIRE, + Kind.PASSWORD_HISTORY, Kind.PASSWORD_HISTORY, Kind.PASSWORD_REUSE_INTERVAL, + Kind.PASSWORD_REUSE_INTERVAL, Kind.PASSWORD_REQUIRE_CURRENT, + Kind.PASSWORD_REQUIRE_CURRENT, Kind.PASSWORD_REQUIRE_CURRENT, + Kind.PASSWORD_LOCK_TIME, Kind.ACCOUNT_UNLOCK, Kind.ACCOUNT_LOCK); + assertThat(options).extracting(PasswordLockOption::getSetting).containsExactly( + Setting.NEVER, Setting.DEFAULT, null, Setting.DEFAULT, null, Setting.DEFAULT, + null, null, Setting.DEFAULT, Setting.OPTIONAL, Setting.UNBOUNDED, null, null); + assertEquals(5L, options.get(4).getValue()); + assertEquals(365L, options.get(6).getValue()); + } + + @Test + void parsesTlsOptionsWithOptionalAnd() throws JSQLParserException { + String sql = "CREATE USER 'app'@'%' REQUIRE CIPHER 'EDH-RSA-DES-CBC3-SHA' " + + "AND ISSUER '/C=SE/O=MySQL' SUBJECT '/CN=app'"; + List options = + roundTrip(CreateUser.class, sql, Dialect.MYSQL).getOptions().getTlsOptions(); + + assertThat(options).extracting(TlsOption::getKind).containsExactly(TlsOption.Kind.CIPHER, + TlsOption.Kind.ISSUER, TlsOption.Kind.SUBJECT); + assertThat(options).extracting(TlsOption::isAndKeyword).containsExactly(false, true, + false); + assertEquals("/CN=app", options.get(2).getValue().getValue()); + + assertEquals(TlsOption.Kind.X509, + roundTrip(CreateUser.class, "CREATE USER app REQUIRE X509", Dialect.MYSQL) + .getOptions().getTlsOptions().get(0).getKind()); + assertEquals(TlsOption.Kind.NONE, + roundTrip(CreateUser.class, "CREATE USER app REQUIRE NONE", Dialect.MYSQL) + .getOptions().getTlsOptions().get(0).getKind()); + } + + @Test + void parsesResourceLimitsAndAttribute() throws JSQLParserException { + String sql = "CREATE USER app DEFAULT ROLE r1, 'r2'@'%' WITH MAX_QUERIES_PER_HOUR 10 " + + "MAX_UPDATES_PER_HOUR 5 MAX_CONNECTIONS_PER_HOUR 2 MAX_USER_CONNECTIONS 1 " + + "ATTRIBUTE '{\"team\": \"ops\"}'"; + UserOptions options = roundTrip(CreateUser.class, sql, Dialect.MYSQL).getOptions(); + + assertEquals(List.of("r1", "'r2'@'%'"), names(options.getDefaultRoles().getRoles())); + assertThat(options.getResourceLimits()).extracting(ResourceLimit::getKind) + .containsExactly(ResourceLimit.Kind.MAX_QUERIES_PER_HOUR, + ResourceLimit.Kind.MAX_UPDATES_PER_HOUR, + ResourceLimit.Kind.MAX_CONNECTIONS_PER_HOUR, + ResourceLimit.Kind.MAX_USER_CONNECTIONS); + assertEquals("{\"team\": \"ops\"}", options.getAttribute().getValue()); + assertNull(options.getComment()); + } + + @Test + void parsesMariaDbAccountOptions() throws JSQLParserException { + CreateUser createUser = roundTrip(CreateUser.class, + "CREATE USER 'app'@'%' IDENTIFIED BY 'x' REQUIRE SSL WITH MAX_USER_CONNECTIONS 5 " + + "ACCOUNT LOCK PASSWORD EXPIRE INTERVAL 30 DAY", + Dialect.MARIADB); + + assertEquals(2, createUser.getOptions().getPasswordLockOptions().size()); + } + + @Test + void mySqlOptionsAfterAnUnquotedNameSelectCreateUser() throws JSQLParserException { + for (String sql : List.of("CREATE USER app WITH MAX_USER_CONNECTIONS 1", + "CREATE USER app PASSWORD EXPIRE", "CREATE USER app PASSWORD HISTORY 2", + "CREATE USER app ACCOUNT LOCK", "CREATE USER app COMMENT 'c'")) { + assertInstanceOf(CreateUser.class, CCJSqlParserUtil.parse(sql), sql); + } + // PostgreSQL role attributes keep the PostgreSQL CREATE USER alias + assertInstanceOf(CreateRole.class, CCJSqlParserUtil.parse("CREATE USER app WITH LOGIN")); + assertInstanceOf(CreateRole.class, CCJSqlParserUtil.parse("CREATE USER app PASSWORD 'x'")); + assertInstanceOf(CreateRole.class, CCJSqlParserUtil.parse("CREATE USER app", + parser -> parser.withDialect(Dialect.POSTGRESQL))); + } + + @Test + void omittedOptionsRenderNothing() throws JSQLParserException { + CreateUser createUser = roundTrip(CreateUser.class, "CREATE USER app", Dialect.MYSQL); + + assertTrue(createUser.getOptions().isEmpty()); + createUser.setOptions(null); + assertTrue(createUser.getOptions().isEmpty()); + assertEquals("CREATE USER app", createUser.toString()); + } + + @Test + void buildsOptions() { + CreateUser createUser = new CreateUser() + .addAccounts(new UserAccount().withUser(new StringValue("app")) + .withHost(new StringValue("%"))) + .withOptions(new UserOptions() + .addTlsOptions(new TlsOption(TlsOption.Kind.SUBJECT, + new StringValue("/CN=app"))) + .addResourceLimits( + new ResourceLimit(ResourceLimit.Kind.MAX_USER_CONNECTIONS, 3)) + .addPasswordLockOptions( + new PasswordLockOption(Kind.PASSWORD_EXPIRE).withValue(30L), + new PasswordLockOption(Kind.ACCOUNT_LOCK)) + .withComment(new StringValue("svc"))); + + assertDeparse(createUser, "CREATE USER 'app'@'%' REQUIRE SUBJECT '/CN=app' " + + "WITH MAX_USER_CONNECTIONS 3 PASSWORD EXPIRE INTERVAL 30 DAY ACCOUNT LOCK " + + "COMMENT 'svc'"); + assertEquals("REQUIRE SUBJECT '/CN=app' WITH MAX_USER_CONNECTIONS 3 " + + "PASSWORD EXPIRE INTERVAL 30 DAY ACCOUNT LOCK COMMENT 'svc'", + createUser.getOptions().toString()); + } + + @Test + void reportsSchemaChangeWithoutTables() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("CREATE USER app REQUIRE SSL ACCOUNT LOCK"); + + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(TablesNamesFinder.findTables(statement.toString())).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE USER app DEFAULT ROLE ALL", + "CREATE USER app DEFAULT ROLE NONE", + "CREATE USER app REQUIRE AND SSL", + "CREATE USER app REQUIRE CIPHER", + "CREATE USER app REQUIRE SSL 'x'", + "CREATE USER app REQUIRE NONE AND SSL", + "CREATE USER app PASSWORD EXPIRE INTERVAL 5 MONTH", + "CREATE USER app PASSWORD HISTORY", + "CREATE USER app COMMENT 'x' ATTRIBUTE '{}'", + "CREATE USER app ACCOUNT LOCK REQUIRE SSL", + "CREATE USER app IDENTIFIED BY 'x' REPLACE 'y'", + "CREATE USER app IDENTIFIED BY 'x' RETAIN CURRENT PASSWORD", + "CREATE USER app DISCARD OLD PASSWORD" + }) + void rejectsOptionsOutsideTheGrammar(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } + + @Test + void optionWordsRemainIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT account, require, expire, reuse, optional, retain, " + + "old, unlock, ssl, x509, cipher, issuer, subject, attribute, " + + "failed_login_attempts, password_lock_time, max_user_connections FROM t"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java b/src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java new file mode 100644 index 0000000000..d92850a0a0 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java @@ -0,0 +1,140 @@ +/*- + * #%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.statement.create.user; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.MySqlDefiner; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import java.util.stream.Stream; + +class MySqlAccountNameTest { + + private static final Consumer MYSQL = p -> p.withDialect(Dialect.MYSQL); + private static final Consumer NO_DIALECT = p -> { + }; + + /** SQL spelling, user value, user quote, host value, host quote. */ + static Stream accounts() { + return Stream.of( + Arguments.of("app", "app", "", null, null), + Arguments.of("app@localhost", "app", "", "localhost", ""), + Arguments.of("app@'%'", "app", "", "%", "'"), + Arguments.of("app@'10.0.0.%'", "app", "", "10.0.0.%", "'"), + Arguments.of("'app'@localhost", "app", "'", "localhost", ""), + Arguments.of("'app'@'%'", "app", "'", "%", "'"), + Arguments.of("`app`@`%`", "app", "`", "%", "`"), + Arguments.of("\"app\"@\"%\"", "app", "\"", "%", "\""), + Arguments.of("'app'", "app", "'", null, null), + Arguments.of("`my app`@'%'", "my app", "`", "%", "'")); + } + + @ParameterizedTest + @MethodSource("accounts") + void createUserSplitsEveryAccountSpelling(String account, String user, String userQuote, + String host, String hostQuote) throws JSQLParserException { + for (Consumer mode : java.util.Arrays.asList(MYSQL, NO_DIALECT)) { + String sql = "CREATE USER " + account + " IDENTIFIED BY 'x'"; + CreateUser createUser = assertInstanceOf(CreateUser.class, + assertSqlCanBeParsedAndDeparsed(sql, true, mode)); + assertEquals(sql, createUser.toString()); + assertEquals(sql, deparse(createUser)); + assertPart(createUser.getAccounts().get(0).getUser(), user, userQuote); + assertPart(createUser.getAccounts().get(0).getHost(), host, hostQuote); + } + } + + @ParameterizedTest + @MethodSource("accounts") + void definerSplitsEveryAccountSpelling(String account, String user, String userQuote, + String host, String hostQuote) throws JSQLParserException { + String view = "CREATE DEFINER = " + account + " VIEW v AS SELECT 1"; + CreateView createView = assertInstanceOf(CreateView.class, + assertSqlCanBeParsedAndDeparsed(view, true, MYSQL)); + MySqlDefiner definer = createView.getMySqlOptions().getDefiner(); + assertPart(definer.getUser(), user, userQuote); + assertPart(definer.getHost(), host, hostQuote); + + String trigger = "CREATE DEFINER = " + account + + " TRIGGER trg BEFORE INSERT ON t FOR EACH ROW SET NEW.a = 1"; + CreateTrigger createTrigger = assertInstanceOf(CreateTrigger.class, + assertSqlCanBeParsedAndDeparsed(trigger, true, MYSQL)); + assertEquals(trigger, createTrigger.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE USER app@", "CREATE USER @host", "CREATE USER 'app'@"}) + void rejectsMalformedAccounts(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL)); + } + + @org.junit.jupiter.api.Test + void keepsDoubleQuotesWithoutDialect() throws JSQLParserException { + // before, the double quotes were dropped and the account rendered as app@% + String sql = "CREATE USER \"app\"@\"%\" IDENTIFIED BY 'x'"; + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + assertEquals(sql, CCJSqlParserUtil.parse(sql, MYSQL).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP USER IF EXISTS CURRENT_USER()", + "RENAME USER CURRENT_USER() TO app2", "SET PASSWORD FOR CURRENT_USER() = 'x'", + "GRANT 'reader' TO CURRENT_USER()"}) + void userListsTakeCurrentUserWithParentheses(String sql) throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed(sql, true, MYSQL); + assertEquals(sql, CCJSqlParserUtil.parse(sql, MYSQL).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE ROLE CURRENT_USER()", "DROP ROLE CURRENT_USER()", + "SET ROLE CURRENT_USER()", "SET DEFAULT ROLE ALL TO CURRENT_USER()", + "GRANT CURRENT_USER() TO app", "ALTER USER app DEFAULT ROLE CURRENT_USER()"}) + void roleListsRejectCurrentUserWithParentheses(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL)); + } + + @org.junit.jupiter.api.Test + void definerCurrentUserIsUnchanged() throws JSQLParserException { + CreateView view = assertInstanceOf(CreateView.class, assertSqlCanBeParsedAndDeparsed( + "CREATE DEFINER = CURRENT_USER() VIEW v AS SELECT 1", true, MYSQL)); + assertNull(view.getMySqlOptions().getDefiner().getHost()); + } + + private static void assertPart(StringValue part, String value, String quote) { + if (value == null) { + assertNull(part); + return; + } + assertEquals(value, part.getValue()); + assertEquals(quote, part.getQuoteStr()); + } + + private static String deparse(CreateUser createUser) { + StringBuilder builder = new StringBuilder(); + createUser.accept(new StatementDeParser(builder), null); + return builder.toString(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/delete/DeleteTest.java b/src/test/java/net/sf/jsqlparser/statement/delete/DeleteTest.java index 2b7bab5e96..f90eeea8e8 100644 --- a/src/test/java/net/sf/jsqlparser/statement/delete/DeleteTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/delete/DeleteTest.java @@ -74,6 +74,20 @@ public void testDeleteDoesNotAllowLimitOffset() { () -> parserManager.parse(new StringReader(statement))); } + @Test + public void testDeleteRequiresTargetTable() { + // a missing target must not parse into a Delete whose toString() + // prints a literal "null" for the absent table + for (String statement : new String[] { + "DELETE", + "DELETE FROM", + "DELETE FROM WHERE a = 1", + "DELETE FROM USING t"}) { + assertThrows(JSQLParserException.class, + () -> parserManager.parse(new StringReader(statement))); + } + } + @Test public void testDeleteWithOrderBy() throws JSQLParserException { String stmt = "DELETE FROM tablename WHERE a = 1 AND b = 1 ORDER BY col"; diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/DropIndexTableTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/DropIndexTableTest.java new file mode 100644 index 0000000000..0319e6de51 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/drop/DropIndexTableTest.java @@ -0,0 +1,132 @@ +/*- + * #%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.statement.drop; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.DropDeParser; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DropIndexTableTest { + @ParameterizedTest + @ValueSource(strings = {"t", "app.t", "`t.x`", "`s.p`.`t.x`", "`한글`"}) + void mysqlOwnersAndOptions(String owner) throws Exception { + for (String suffix : List.of("", " ALGORITHM = INPLACE LOCK = NONE", " LOCK SHARED", + " ALGORITHM COPY", " LOCK EXCLUSIVE ALGORITHM DEFAULT", " LOCK DEFAULT")) { + String sql = "DROP INDEX `i.x` ON " + owner + suffix; + for (Drop drop : roundTrip(sql, Dialect.MYSQL)) { + assertEquals("`i.x`", drop.getName().getName()); + assertEquals(owner, drop.getTable().getFullyQualifiedName()); + assertEquals(List.of("ON", owner), drop.getParameters().subList(0, 2)); + assertEquals(Set.of(owner), new TablesNamesFinder().getTables(drop)); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"[t]", "[dbo].[t]", "[db].[dbo].[t]"}) + void sqlServerOwners(String owner) throws Exception { + for (Drop drop : roundTrip("DROP INDEX IF EXISTS [ix] ON " + owner, Dialect.SQLSERVER)) { + assertEquals(owner, drop.getTable().getFullyQualifiedName()); + assertTrue(drop.isIfExists()); + } + } + + @Test + void mutationAndLegacyParametersStayConsistent() throws Exception { + Drop drop = parse("DROP INDEX i ON t LOCK NONE", Dialect.MYSQL); + drop.getTable().setName("renamed"); + assertEquals(List.of("ON", "renamed", "LOCK", "NONE"), drop.getParameters()); + List snapshot = drop.getParameters(); + snapshot.set(1, "ignored"); + assertEquals("renamed", drop.getTable().getName()); + drop.addParameters("ALGORITHM", "DEFAULT"); + assertEquals("DROP INDEX i ON renamed LOCK NONE ALGORITHM DEFAULT", drop.toString()); + drop.setTable(null); + assertEquals("DROP INDEX i LOCK NONE ALGORITHM DEFAULT", drop.toString()); + drop.withTable(new Table("next")); + drop.setParameters(List.of("ON", "legacy")); + assertNull(drop.getTable()); + assertEquals("DROP INDEX i ON legacy", drop.toString()); + assertThrows(IllegalArgumentException.class, () -> drop.setTable(new Table("t"), 99)); + } + + @Test + void builderAndParameterOrder() { + Drop drop = new Drop().withType("INDEX").withName(new Table("i")) + .addParameters("EXTENSION"); + drop.setTable(new Table("t"), 1); + drop.addParameters(List.of("LOCK", "NONE")); + assertEquals("DROP INDEX i EXTENSION ON t LOCK NONE", drop.toString()); + assertEquals(List.of("EXTENSION", "ON", "t", "LOCK", "NONE"), drop.getParameters()); + } + + @Test + void doesNotInventOwnersOrDiscoverCatalogNamesAsTables() throws Exception { + for (String sql : List.of("DROP INDEX public.i", "DROP SCHEMA s", "DROP SEQUENCE seq", + "DROP FUNCTION f(integer)", "DROP TRIGGER tr")) { + Drop drop = parse(sql, Dialect.POSTGRESQL); + assertNull(drop.getTable()); + assertTrue(new TablesNamesFinder().getTables(drop).isEmpty()); + } + assertEquals(Set.of("a", "b"), new TablesNamesFinder().getTables( + parse("DROP TABLE a, b", Dialect.POSTGRESQL))); + assertEquals(Set.of("v"), new TablesNamesFinder().getTables( + parse("DROP MATERIALIZED VIEW v", Dialect.POSTGRESQL))); + } + + @Test + void customDeparserRewritesOwnerOnly() throws Exception { + Drop drop = parse("DROP INDEX i ON t LOCK NONE", Dialect.MYSQL); + StringBuilder builder = new StringBuilder(); + SelectDeParser tables = new SelectDeParser() { + @Override + public StringBuilder visit(Table table, S context) { + assertEquals("ctx", context); + return getBuilder().append("new_t"); + } + }; + drop.accept(new StatementDeParser(new ExpressionDeParser(), tables, builder), "ctx"); + assertEquals("DROP INDEX i ON new_t LOCK NONE", builder.toString()); + assertEquals("DROP INDEX i ON t LOCK NONE", drop.toString()); + } + + @Test + void malformedOwnersAndStatementBoundaries() throws Exception { + for (String sql : List.of("DROP INDEX i ON", "DROP INDEX i ON t ON u")) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL)); + } + assertEquals(2, CCJSqlParserUtil.parseStatements( + "DROP INDEX i ON t; LOCK TABLE u IN SHARE MODE").size()); + } + + private static Drop parse(String sql, Dialect dialect) throws JSQLParserException { + return (Drop) CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + } + + private static List roundTrip(String sql, Dialect dialect) throws Exception { + Drop drop = parse(sql, dialect); + StringBuilder builder = new StringBuilder(); + new DropDeParser(builder).deParse(drop); + assertEquals(drop.toString(), builder.toString()); + return List.of(drop, parse(drop.toString(), dialect), parse(builder.toString(), dialect)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/DropObjectTypeTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/DropObjectTypeTest.java new file mode 100644 index 0000000000..aeff84ef4a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/drop/DropObjectTypeTest.java @@ -0,0 +1,294 @@ +/*- + * #%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.statement.drop; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DropObjectTypeTest { + + @ParameterizedTest + @ValueSource(strings = {"DROP TRIGGER IF EXISTS trg ON users CASCADE", + "DROP TRIGGER trg ON app.users", "DROP RULE r1 ON v", + "DROP RULE IF EXISTS r1 ON v RESTRICT"}) + void triggerAndRuleOwnersAreTables(String sql) throws JSQLParserException { + for (Drop drop : roundTrip(sql, Dialect.POSTGRESQL)) { + assertEquals(sql, drop.toString()); + String owner = sql.replaceAll(".* ON (\\S+).*", "$1"); + assertEquals(owner, drop.getTable().getFullyQualifiedName()); + assertEquals(Set.of(owner), new TablesNamesFinder().getTables(drop)); + } + } + + @Test + void ownerTableReplacesLegacyOnTokens() throws JSQLParserException { + Drop drop = parse("DROP TRIGGER IF EXISTS trg ON users CASCADE", Dialect.POSTGRESQL); + assertEquals("trg", drop.getName().getName()); + assertEquals(CascadeBehavior.CASCADE, drop.getBehavior()); + assertEquals(List.of("ON", "users", "CASCADE"), drop.getParameters()); + + drop.getTable().setName("accounts"); + drop.setBehavior(CascadeBehavior.RESTRICT); + assertEquals("DROP TRIGGER IF EXISTS trg ON accounts RESTRICT", drop.toString()); + assertEquals(List.of("ON", "accounts", "RESTRICT"), drop.getParameters()); + + StringBuilder builder = new StringBuilder(); + SelectDeParser tables = new SelectDeParser() { + @Override + public StringBuilder visit(Table table, S context) { + return getBuilder().append("renamed"); + } + }; + drop.accept(new StatementDeParser(new ExpressionDeParser(), tables, builder), null); + assertEquals("DROP TRIGGER IF EXISTS trg ON renamed RESTRICT", builder.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP TRIGGER safety ON DATABASE", "DROP TRIGGER safety ON ALL SERVER"}) + void sqlServerTriggerScopesAreNotTables(String sql) throws JSQLParserException { + Drop drop = parse(sql, Dialect.SQLSERVER); + assertNull(drop.getTable()); + assertTrue(new TablesNamesFinder().getTables(drop).isEmpty()); + assertEquals(sql, drop.toString()); + } + + @Test + void cascadeAndRestrictAreTyped() throws JSQLParserException { + Drop table = parse("DROP TABLE IF EXISTS t1, t2 RESTRICT", Dialect.MYSQL); + assertEquals(CascadeBehavior.RESTRICT, table.getBehavior()); + assertEquals(List.of("RESTRICT"), table.getParameters()); + + Drop index = parse("DROP INDEX a_idx, b_idx CASCADE", Dialect.POSTGRESQL); + assertEquals(CascadeBehavior.CASCADE, index.getBehavior()); + index.setBehavior(null); + assertEquals("DROP INDEX a_idx, b_idx", index.toString()); + assertNull(index.getParameters()); + + Drop plain = parse("DROP SCHEMA s", Dialect.POSTGRESQL); + assertNull(plain.getBehavior()); + assertNull(plain.getParameters()); + } + + @Test + void rawParametersAndBehaviorFollowExplicitRules() throws JSQLParserException { + Drop drop = parse("DROP INDEX i ON t CASCADE", Dialect.MYSQL); + drop.addParameters("LOCK", "NONE"); + assertEquals(List.of("ON", "t", "LOCK", "NONE", "CASCADE"), drop.getParameters()); + assertEquals("DROP INDEX i ON t LOCK NONE CASCADE", drop.toString()); + + drop.setParameters(List.of("RESTRICT")); + assertNull(drop.getBehavior()); + assertNull(drop.getTable()); + assertEquals("DROP INDEX i RESTRICT", drop.toString()); + + // Raw tokens are not parsed: the builder spelling renders the same SQL. + Drop legacy = new Drop().withType("VIEW").withName(new Table("v")).addParameters("CASCADE"); + assertNull(legacy.getBehavior()); + assertEquals("DROP VIEW v CASCADE", legacy.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP TYPE IF EXISTS mood CASCADE", "DROP TYPE mood, app.color", + "DROP DOMAIN d1", "DROP DOMAIN IF EXISTS d1 RESTRICT", "DROP EVENT TRIGGER etg", + "DROP EVENT TRIGGER IF EXISTS etg CASCADE", "DROP TABLESPACE IF EXISTS fast", + "DROP DATABASE IF EXISTS mydb WITH (FORCE)", "DROP DATABASE mydb WITH (FORCE)"}) + void postgreSqlObjectTypes(String sql) throws JSQLParserException { + for (Drop drop : roundTrip(sql, Dialect.POSTGRESQL)) { + assertEquals(sql, drop.toString()); + assertNotEquals(Drop.ObjectType.OTHER, drop.getObjectType()); + assertTrue(new TablesNamesFinder().getTables(drop).isEmpty()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"DROP ACCESS METHOD heptree", + "DROP ACCESS METHOD IF EXISTS heptree CASCADE", "DROP PROCEDURAL LANGUAGE plsample", + "DROP PROCEDURAL LANGUAGE IF EXISTS plsample RESTRICT", + "DROP LANGUAGE IF EXISTS plsample CASCADE", + "DROP OPERATOR CLASS IF EXISTS public.myops USING btree CASCADE", + "DROP OPERATOR FAMILY myfam USING hash", + "DROP TEXT SEARCH CONFIGURATION IF EXISTS my_cfg CASCADE", + "DROP TEXT SEARCH DICTIONARY english_stem, public.french_stem", + "DROP TEXT SEARCH PARSER my_parser", + "DROP TEXT SEARCH TEMPLATE IF EXISTS my_tmpl RESTRICT"}) + void postgreSqlMultiWordObjectTypes(String sql) throws JSQLParserException { + for (Drop drop : roundTrip(sql, Dialect.POSTGRESQL)) { + assertEquals(sql, drop.toString()); + assertNotEquals(Drop.ObjectType.OTHER, drop.getObjectType()); + assertTrue(new TablesNamesFinder().getTables(drop).isEmpty()); + } + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + parser -> parser.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void multiWordObjectTypeFields() throws JSQLParserException { + Drop accessMethod = + parse("DROP ACCESS METHOD IF EXISTS heptree CASCADE", Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.ACCESS_METHOD, accessMethod.getObjectType()); + assertEquals("ACCESS METHOD", accessMethod.getType()); + assertEquals(List.of("heptree"), names(accessMethod)); + assertTrue(accessMethod.isIfExists()); + assertEquals(CascadeBehavior.CASCADE, accessMethod.getBehavior()); + + Drop language = parse("DROP PROCEDURAL LANGUAGE plsample", Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.LANGUAGE, language.getObjectType()); + assertTrue(language.isProcedural()); + assertEquals(List.of("plsample"), names(language)); + language.setProcedural(false); + assertEquals("DROP LANGUAGE plsample", language.toString()); + assertFalse(parse("DROP LANGUAGE plsample", Dialect.POSTGRESQL).isProcedural()); + + Drop operatorClass = parse("DROP OPERATOR CLASS IF EXISTS public.myops USING btree", + Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.OPERATOR_CLASS, operatorClass.getObjectType()); + assertEquals(List.of("public.myops"), names(operatorClass)); + assertEquals("btree", operatorClass.getAccessMethod()); + assertNull(operatorClass.getParameters()); + operatorClass.setAccessMethod("hash"); + assertEquals("DROP OPERATOR CLASS IF EXISTS public.myops USING hash", + operatorClass.toString()); + assertEquals(Drop.ObjectType.OPERATOR_FAMILY, + parse("DROP OPERATOR FAMILY myfam USING gist", Dialect.POSTGRESQL).getObjectType()); + + Drop dictionary = parse("DROP TEXT SEARCH DICTIONARY english_stem, public.french_stem", + Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.TEXT_SEARCH_DICTIONARY, dictionary.getObjectType()); + assertEquals(List.of("english_stem", "public.french_stem"), names(dictionary)); + + assertEquals("DROP OPERATOR FAMILY f USING btree CASCADE", + new Drop().withObjectType(Drop.ObjectType.OPERATOR_FAMILY) + .withName(new Table("f")).withAccessMethod("btree") + .withBehavior(CascadeBehavior.CASCADE).toString()); + assertEquals("DROP PROCEDURAL LANGUAGE l", new Drop() + .withObjectType(Drop.ObjectType.LANGUAGE).withProcedural(true) + .withName(new Table("l")).toString()); + } + + @Test + void multiWordTypeWordsRemainNames() throws JSQLParserException { + assertEquals(List.of("procedural"), + names(parse("DROP LANGUAGE procedural", Dialect.POSTGRESQL))); + assertEquals(List.of("method"), + names(parse("DROP ACCESS METHOD method", Dialect.POSTGRESQL))); + assertEquals(List.of("access"), names(parse("DROP TABLE access", Dialect.POSTGRESQL))); + assertEquals(List.of("family"), + names(parse("DROP OPERATOR CLASS family USING btree", Dialect.POSTGRESQL))); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP OPERATOR CLASS myops", "DROP OPERATOR FAMILY f1, f2 USING btree", + "DROP OPERATOR CLASS myops USING", "DROP ACCESS METHOD", + "DROP ACCESS METHOD IF EXISTS", "DROP TEXT SEARCH INDEX x", "DROP TEXT SEARCH", + "DROP PROCEDURAL LANGUAGE", "DROP ACCESS METHOD heptree USING btree"}) + void rejectsIncompleteMultiWordTypes(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.POSTGRESQL), sql); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP EVENT IF EXISTS ev1", "DROP EVENT db1.ev1", "DROP TABLESPACE ts1", + "DROP TABLESPACE ts1 ENGINE = InnoDB", "DROP UNDO TABLESPACE ts1", + "DROP UNDO TABLESPACE ts1 ENGINE InnoDB"}) + void mySqlObjectTypes(String sql) throws JSQLParserException { + for (Drop drop : roundTrip(sql, Dialect.MYSQL)) { + assertEquals(sql, drop.toString()); + } + } + + @Test + void objectTypeFields() throws JSQLParserException { + Drop eventTrigger = parse("DROP EVENT TRIGGER etg", Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.EVENT_TRIGGER, eventTrigger.getObjectType()); + assertEquals("EVENT TRIGGER", eventTrigger.getType()); + assertEquals("etg", eventTrigger.getName().getName()); + + Drop event = parse("DROP EVENT IF EXISTS ev1", Dialect.MYSQL); + assertEquals(Drop.ObjectType.EVENT, event.getObjectType()); + + Drop type = parse("DROP TYPE mood, app.color CASCADE", Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.TYPE, type.getObjectType()); + assertEquals("app", type.getNames().get(1).getSchemaName()); + + Drop tablespace = parse("DROP UNDO TABLESPACE ts1 ENGINE=InnoDB", Dialect.MYSQL); + assertEquals(Drop.ObjectType.TABLESPACE, tablespace.getObjectType()); + assertTrue(tablespace.isUndo()); + assertEquals("InnoDB", tablespace.getEngine()); + assertTrue(tablespace.isEngineUseEquals()); + assertEquals("DROP UNDO TABLESPACE ts1 ENGINE = InnoDB", tablespace.toString()); + tablespace.withUndo(false).withEngine("NDB").setEngineUseEquals(false); + assertEquals("DROP TABLESPACE ts1 ENGINE NDB", tablespace.toString()); + + Drop database = parse("DROP DATABASE mydb (FORCE)", Dialect.POSTGRESQL); + assertTrue(database.isForce()); + assertEquals("DROP DATABASE mydb WITH (FORCE)", database.toString()); + database.setForce(false); + assertEquals("DROP DATABASE mydb", database.toString()); + + assertEquals("DROP DATABASE IF EXISTS d WITH (FORCE)", new Drop() + .withObjectType(Drop.ObjectType.DATABASE).withIfExists(true) + .withName(new Table("d")).withForce(true).toString()); + } + + @Test + void clauseWordsRemainNames() throws JSQLParserException { + assertEquals("engine", + parse("DROP TABLESPACE engine", Dialect.MYSQL).getName().getName()); + assertEquals("undo", parse("DROP TABLE undo", Dialect.MYSQL).getName().getName()); + assertEquals("owned", parse("DROP TABLE owned", Dialect.POSTGRESQL).getName().getName()); + assertEquals(2, CCJSqlParserUtil.parseStatements( + "DROP DATABASE d; WITH x AS (SELECT 1) SELECT * FROM x", + parser -> parser.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void rejectsMisplacedClauses() { + for (String sql : List.of("DROP TABLE t WITH (FORCE)", "DROP DATABASE d WITH (FORCE", + "DROP TABLE t ENGINE = InnoDB", "DROP TABLESPACE ts ENGINE", + "DROP TABLE t CASCADE x", + "DROP TRIGGER t ON a ON b", "DROP EVENT TRIGGER", "DROP TYPE")) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL), sql); + } + } + + private static List names(Drop drop) { + return drop.getNames().stream().map(Table::getFullyQualifiedName) + .collect(Collectors.toList()); + } + + private static Drop parse(String sql, Dialect dialect) throws JSQLParserException { + return (Drop) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static List roundTrip(String sql, Dialect dialect) throws JSQLParserException { + Drop drop = (Drop) assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(dialect)); + StringBuilder builder = new StringBuilder(); + drop.accept(new StatementDeParser(builder), null); + assertEquals(drop.toString(), builder.toString()); + return List.of(drop, parse(drop.toString(), dialect)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/DropRoleTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/DropRoleTest.java new file mode 100644 index 0000000000..4d98a3b42f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/drop/DropRoleTest.java @@ -0,0 +1,178 @@ +/*- + * #%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.statement.drop; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.statement.role.ReassignOwned; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DropRoleTest { + + @ParameterizedTest + @ValueSource(strings = {"DROP USER IF EXISTS 'app'@'%', 'u2'@'localhost'", "DROP USER app", + "DROP USER 'app'@'%'", "DROP USER app@localhost, `u`@`h`", + "DROP USER IF EXISTS app@'%', 'app'@localhost", + "DROP ROLE IF EXISTS 'reader'", + "DROP ROLE 'r'@'h', r2"}) + void mySqlAccounts(String sql) throws JSQLParserException { + for (Statement statement : roundTrip(sql, Dialect.MYSQL)) { + Drop drop = (Drop) statement; + assertEquals(sql, drop.toString()); + assertTrue(drop.getObjectType().hasAccountTargets()); + assertTrue(new TablesNamesFinder().getTables(drop).isEmpty()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"DROP USER IF EXISTS alice", "DROP USER \"Alice\", bob", + "DROP GROUP staff", "DROP ROLE IF EXISTS app_read, app_rw", + "DROP OWNED BY old_role", "DROP OWNED BY old_role, \"New\" CASCADE", + "DROP OWNED BY CURRENT_USER RESTRICT"}) + void postgreSqlRoles(String sql) throws JSQLParserException { + for (Statement statement : roundTrip(sql, Dialect.POSTGRESQL)) { + assertEquals(sql, statement.toString()); + } + } + + @Test + void accountParts() throws JSQLParserException { + Drop drop = parse("DROP USER IF EXISTS 'app'@'%', u2", Dialect.MYSQL); + assertEquals(Drop.ObjectType.USER, drop.getObjectType()); + assertTrue(drop.isIfExists()); + UserAccount first = drop.getAccounts().get(0); + assertEquals("app", first.getUser().getValue()); + assertEquals("%", first.getHost().getValue()); + assertNull(drop.getAccounts().get(1).getHost()); + + first.setHost(new StringValue("localhost")); + assertEquals("DROP USER IF EXISTS 'app'@'localhost', u2", drop.toString()); + assertEquals(List.of("'app'@'localhost'", "u2"), names(drop)); + assertThrows(UnsupportedOperationException.class, + () -> drop.getAccounts().add(new UserAccount())); + + Drop role = parse("DROP ROLE \"Admin\"", Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.ROLE, role.getObjectType()); + StringValue admin = role.getAccounts().get(0).getUser(); + assertEquals("Admin", admin.getValue()); + assertEquals("\"", admin.getQuoteStr()); + assertNull(role.getAccounts().get(0).getHost()); + assertEquals("\"Admin\"", role.getName().getName()); + assertEquals("DROP ROLE \"Admin\"", role.toString()); + } + + @Test + void dropOwnedKeepsRolesOutOfTheName() throws JSQLParserException { + Drop drop = parse("DROP OWNED BY old_role, other CASCADE", Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.OWNED, drop.getObjectType()); + assertEquals(List.of("old_role", "other"), drop.getAccounts().stream() + .map(Object::toString).collect(Collectors.toList())); + assertEquals(List.of("old_role", "other"), names(drop)); + assertEquals(CascadeBehavior.CASCADE, drop.getBehavior()); + assertEquals(List.of("CASCADE"), drop.getParameters()); + + drop.setName(new Table("renamed")); + assertEquals("DROP OWNED BY renamed CASCADE", drop.toString()); + + assertEquals("DROP OWNED BY r", new Drop().withObjectType(Drop.ObjectType.OWNED) + .addAccounts(new UserAccount().withUser(new StringValue("r").setQuoteStr(""))) + .toString()); + } + + @Test + void reassignOwned() throws JSQLParserException { + ReassignOwned statement = (ReassignOwned) CCJSqlParserUtil.parse( + "REASSIGN OWNED BY old_role, \"Old\" TO new_role", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(List.of("old_role", "\"Old\""), statement.getRoles().stream() + .map(Object::toString).collect(Collectors.toList())); + assertEquals("new_role", statement.getNewRole().toString()); + assertEquals("Old", statement.getRoles().get(1).getUser().getValue()); + assertEquals("\"", statement.getRoles().get(1).getUser().getQuoteStr()); + + statement.setNewRole(new UserAccount().withUser(new StringValue("CURRENT_USER") + .setQuoteStr(""))); + assertEquals("REASSIGN OWNED BY old_role, \"Old\" TO CURRENT_USER", statement.toString()); + assertThrows(UnsupportedOperationException.class, + () -> statement.getRoles().add(new UserAccount())); + + ReassignOwned built = new ReassignOwned().withRoles(statement.getRoles()) + .withNewRole(statement.getNewRole()); + assertEquals(statement.toString(), built.toString()); + + assertTrue(new TablesNamesFinder().getTables((Statement) statement).isEmpty()); + assertNull(statement.accept(new StatementVisitorAdapter(), null)); + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(parse("DROP OWNED BY r", Dialect.POSTGRESQL).getFeatures().getCertain()) + .contains(StmtFeature.MODIFIES_SCHEMA, StmtFeature.MODIFIES_DATA); + } + + @Test + void ownershipStatementsInScripts() throws JSQLParserException { + List statements = CCJSqlParserUtil.parseStatements( + "REASSIGN OWNED BY a TO b; DROP OWNED BY a; DROP ROLE a", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(3, statements.size()); + assertInstanceOf(ReassignOwned.class, statements.get(0)); + assertEquals("reassign", ((Drop) parse("DROP TABLE reassign", Dialect.POSTGRESQL)) + .getName().getName()); + assertEquals("reassign", CCJSqlParserUtil.parse("SELECT reassign FROM owned") + .toString().split(" ")[1]); + } + + @Test + void rejectsMalformedRoleStatements() { + for (String sql : List.of("DROP OWNED IF EXISTS BY r", "DROP OWNED BY", "DROP USER", + "DROP USER 'app'@", "REASSIGN OWNED BY a", "REASSIGN OWNED a TO b", + "REASSIGN OWNED BY TO b", "DROP ROLE a.b")) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)), sql); + } + } + + private static List names(Drop drop) { + return drop.getNames().stream().map(Table::getFullyQualifiedName) + .collect(Collectors.toList()); + } + + private static Drop parse(String sql, Dialect dialect) throws JSQLParserException { + return (Drop) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static List roundTrip(String sql, Dialect dialect) + throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(dialect)); + StringBuilder builder = new StringBuilder(); + statement.accept(new StatementDeParser(builder), null); + assertEquals(statement.toString(), builder.toString()); + return List.of(statement, CCJSqlParserUtil.parse(statement.toString(), + parser -> parser.withDialect(dialect))); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/DropRoutineSignatureTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/DropRoutineSignatureTest.java new file mode 100644 index 0000000000..4259d3d071 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/drop/DropRoutineSignatureTest.java @@ -0,0 +1,236 @@ +/*- + * #%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.statement.drop; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DropRoutineSignatureTest { + + @ParameterizedTest + @ValueSource(strings = {"DROP PROCEDURE p1(int)", "DROP PROCEDURE p1()", + "DROP FUNCTION IF EXISTS f2(int), g2(text) CASCADE", + "DROP FUNCTION f2(int), g2(text)", "DROP FUNCTION a, b(int)", + "DROP ROUTINE f4", "DROP ROUTINE IF EXISTS s.f4(IN a int, OUT b text) RESTRICT", + "DROP FUNCTION f(VARIADIC a int[], double precision)", + "DROP AGGREGATE agg(int)", "DROP AGGREGATE agg(*)", + "DROP AGGREGATE IF EXISTS agg(int ORDER BY text), agg2(*) CASCADE", + "DROP FUNCTION IF EXISTS \"fin\".\"restore_fund_data\"(\"in_fund_id\" int8)"}) + void postgreSqlSignaturesSurviveRoundTrips(String sql) throws JSQLParserException { + for (Drop drop : roundTrip(sql, Dialect.POSTGRESQL)) { + assertEquals(sql, drop.toString()); + assertFalse(drop.getRoutines().isEmpty()); + assertTrue(drop.getObjectType().hasRoutineTargets()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"DROP PROCEDURE IF EXISTS p1", "DROP FUNCTION IF EXISTS db1.f1", + "DROP PROCEDURE `db.x`.`p.y`"}) + void mySqlRoutinesWithoutSignatures(String sql) throws JSQLParserException { + for (Drop drop : roundTrip(sql, Dialect.MYSQL)) { + assertEquals(sql, drop.toString()); + assertNull(drop.getRoutines().get(0).getArguments()); + } + } + + @Test + void procedureSignatureIsKeptInTheAst() throws JSQLParserException { + Drop drop = parse("DROP PROCEDURE p1(int)"); + RoutineReference procedure = drop.getRoutines().get(0); + assertEquals("p1", procedure.getName()); + assertEquals(1, procedure.getArguments().size()); + assertEquals("int", procedure.getArguments().get(0).getDataType().toString()); + assertEquals(Drop.ObjectType.PROCEDURE, drop.getObjectType()); + } + + @Test + void everyTargetKeepsItsOwnSignature() throws JSQLParserException { + Drop drop = parse("DROP FUNCTION IF EXISTS f2(int), g2(text), h CASCADE"); + assertEquals(List.of("f2(int)", "g2(text)", "h"), drop.getRoutines().stream() + .map(Object::toString).collect(Collectors.toList())); + assertNull(drop.getRoutines().get(2).getArguments()); + assertTrue(drop.isIfExists()); + assertEquals(CascadeBehavior.CASCADE, drop.getBehavior()); + + RoutineReference.Argument argument = drop.getRoutines().get(1).getArguments().get(0); + argument.setName("label"); + argument.setDataType(new ColDataType("varchar")); + drop.getRoutines().get(0).setName("app.f2"); + assertEquals("DROP FUNCTION IF EXISTS app.f2(int), g2(label varchar), h CASCADE", + drop.toString()); + assertRendersAlike(drop); + } + + @Test + void aggregateSignatureKeepsStarAndOrderBy() throws JSQLParserException { + Drop drop = parse("DROP AGGREGATE agg(int ORDER BY text), agg2(*)"); + assertEquals(Drop.ObjectType.AGGREGATE, drop.getObjectType()); + assertEquals("text", drop.getRoutines().get(0).getOrderByArguments().get(0) + .getDataType().toString()); + assertTrue(drop.getRoutines().get(1).isAllArguments()); + } + + @Test + void legacyNameAndParameterAccessorsAreDerived() throws JSQLParserException { + Drop function = + parse("DROP FUNCTION IF EXISTS \"fin\".\"restore_fund_data\"(in_fund_id int8)"); + Table name = function.getName(); + assertEquals("\"fin\"", name.getSchemaName()); + assertEquals("\"restore_fund_data\"", name.getName()); + assertEquals(List.of("in_fund_id int8"), function.getParamsByType("FUNCTION")); + assertEquals(Map.of("FUNCTION", List.of("in_fund_id int8")), + function.getTypeToParameters()); + assertThrows(UnsupportedOperationException.class, + () -> function.getTypeToParameters().put("FUNCTION", List.of())); + assertThrows(UnsupportedOperationException.class, + () -> function.getNames().add(new Table("x"))); + + // The legacy model holds one signature, written after the last name. + Drop several = parse("DROP FUNCTION a, b(int)"); + assertEquals(List.of("a", "b"), names(several)); + assertEquals(List.of("int"), several.getParamsByType("FUNCTION")); + + assertEquals(List.of("int"), parse("DROP PROCEDURE p1(int)").getParamsByType("PROCEDURE")); + assertEquals(List.of(), parse("DROP FUNCTION f()").getParamsByType("FUNCTION")); + Drop omitted = parse("DROP FUNCTION f"); + assertTrue(omitted.getTypeToParameters().containsKey("FUNCTION")); + assertNull(omitted.getParamsByType("FUNCTION")); + assertEquals(List.of("int"), parse("drop function f(int)").getParamsByType("FUNCTION")); + assertEquals(List.of("*"), parse("DROP AGGREGATE agg(*)").getParamsByType("AGGREGATE")); + } + + @Test + void legacySettersSwitchToTheLegacyRepresentation() throws JSQLParserException { + Drop drop = parse("DROP FUNCTION f(int) CASCADE"); + drop.setName(new Table("g")); + assertTrue(drop.getRoutines().isEmpty()); + assertEquals("DROP FUNCTION g(int) CASCADE", drop.toString()); + drop.getTypeToParameters().put("FUNCTION", List.of("bigint")); + assertEquals("DROP FUNCTION g(bigint) CASCADE", drop.toString()); + + Drop renamed = parse("DROP FUNCTION f(int)"); + renamed.setNames(List.of(new Table("app", "f"), new Table("app", "g"))); + assertEquals("DROP FUNCTION app.f, app.g(int)", renamed.toString()); + + Drop replaced = parse("DROP FUNCTION f(int)"); + replaced.setTypeToParameters(new HashMap<>(Map.of("FUNCTION", List.of("bigint")))); + assertEquals(List.of("f"), names(replaced)); + assertEquals("DROP FUNCTION f(bigint)", replaced.toString()); + + replaced.setRoutines(List.of(routine("f2", "text"))); + assertEquals("DROP FUNCTION f2(text)", replaced.toString()); + assertEquals(Map.of("FUNCTION", List.of("text")), replaced.getTypeToParameters()); + } + + @Test + void buildersRenderBothRepresentations() { + Drop legacy = new Drop().withType("FUNCTION").withName(new Table("myFunc")); + legacy.getTypeToParameters().put("FUNCTION", List.of("integer", "name varchar(255)")); + assertEquals("DROP FUNCTION myFunc(integer, name varchar(255))", legacy.toString()); + assertTrue(legacy.getRoutines().isEmpty()); + + Drop typed = new Drop().withObjectType(Drop.ObjectType.ROUTINE).withIfExists(true) + .addRoutines(routine("r1", "int"), routine("r2")) + .withBehavior(CascadeBehavior.RESTRICT); + assertEquals("DROP ROUTINE IF EXISTS r1(int), r2() RESTRICT", typed.toString()); + assertRendersAlike(typed); + + Drop switched = new Drop().withType("FUNCTION").withName(new Table("old")) + .withRoutines(List.of(routine("f", "int"))); + assertEquals(List.of("f"), names(switched)); + assertEquals("DROP FUNCTION f(int)", switched.toString()); + switched.addRoutines(); + assertEquals("DROP FUNCTION f(int)", switched.toString()); + } + + @Test + void routineNamesAreNotReportedAsTables() throws JSQLParserException { + for (String sql : List.of("DROP FUNCTION f(int)", "DROP PROCEDURE app.p(text)", + "DROP AGGREGATE agg(*)")) { + assertTrue(new TablesNamesFinder().getTables(parse(sql)).isEmpty()); + } + } + + @Test + void rejectsSignaturesOutsideTheirPlace() { + for (String sql : List.of("DROP AGGREGATE agg", "DROP FUNCTION f(*)", + "DROP FUNCTION f(int ORDER BY int)", "DROP TABLE t(int)", "DROP INDEX i(a)", + "DROP FUNCTION f(int) CASCADE RESTRICT", "DROP FUNCTION f(int), ")) { + assertThrows(JSQLParserException.class, () -> parse(sql), sql); + } + } + + @Test + void scriptsKeepFollowingStatements() throws JSQLParserException { + assertEquals(3, CCJSqlParserUtil.parseStatements( + "DROP PROCEDURE p1(int); DROP FUNCTION f(int), g(text) CASCADE; SELECT 1", + parser -> parser.withDialect(Dialect.POSTGRESQL)).size()); + assertSqlCanBeParsedAndDeparsed("DROP FUNCTION myFunc(amount integer, name varchar(255))", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static RoutineReference routine(String name, String... types) { + RoutineReference routine = new RoutineReference(); + routine.setName(name); + List arguments = new ArrayList<>(); + for (String type : types) { + RoutineReference.Argument argument = new RoutineReference.Argument(); + argument.setDataType(new ColDataType(type)); + arguments.add(argument); + } + routine.setArguments(arguments); + return routine; + } + + private static List names(Drop drop) { + return drop.getNames().stream().map(Table::getFullyQualifiedName) + .collect(Collectors.toList()); + } + + private static Drop parse(String sql) throws JSQLParserException { + return parse(sql, Dialect.POSTGRESQL); + } + + private static Drop parse(String sql, Dialect dialect) throws JSQLParserException { + return (Drop) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static void assertRendersAlike(Drop drop) { + StringBuilder builder = new StringBuilder(); + drop.accept(new StatementDeParser(builder), null); + assertEquals(drop.toString(), builder.toString()); + } + + private static List roundTrip(String sql, Dialect dialect) throws JSQLParserException { + Drop drop = (Drop) assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(dialect)); + assertRendersAlike(drop); + return List.of(drop, parse(drop.toString(), dialect)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java index a48b2fe7df..ca35f4640d 100644 --- a/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java @@ -10,10 +10,15 @@ package net.sf.jsqlparser.statement.drop; import java.io.StringReader; +import java.util.List; +import java.util.stream.Collectors; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserManager; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; import static net.sf.jsqlparser.test.TestUtils.*; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -43,7 +48,7 @@ public void testDropIndex() throws JSQLParserException { assertEquals("CASCADE", parsed.getParameters().get(0)); assertEquals(statement, "" + parsed); Drop created = new Drop().withType("INDEX").withName(new Table("myindex")) - .addParameters("CASCADE"); + .withBehavior(CascadeBehavior.CASCADE); assertDeparse(created, statement); assertEqualsObjectTree(parsed, created); } @@ -85,7 +90,7 @@ public void testDropRestrictIssue510() throws JSQLParserException { String statement = "DROP TABLE TABLE2 RESTRICT"; Statement parsed = assertSqlCanBeParsedAndDeparsed(statement); Drop created = new Drop().withType("TABLE").withName(new Table("TABLE2")) - .addParameters(asList("RESTRICT")); + .withBehavior(CascadeBehavior.RESTRICT); assertDeparse(created, statement); assertEqualsObjectTree(parsed, created); } @@ -152,4 +157,62 @@ void dropTemporaryTableTestIssue1712() throws JSQLParserException { String sqlStr = "drop temporary table if exists tmp_MwYT8N0z"; assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + public void testDropIndexAlgorithmAndLockOptionsIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("DROP INDEX i15 ON t ALGORITHM = INPLACE LOCK = NONE"); + assertSqlCanBeParsedAndDeparsed("DROP INDEX i16 ON t ALGORITHM INPLACE"); + assertSqlCanBeParsedAndDeparsed("DROP INDEX i17 ON t LOCK = NONE"); + } + + @Test + public void testDropTableFollowedByLockTableIssue2490() throws JSQLParserException { + // LOCK must not be swallowed as a DROP argument when it starts the next statement. + Statements statements = CCJSqlParserUtil.parseStatements( + "DROP TABLE t1; LOCK TABLE t2 IN SHARE MODE;"); + assertEquals(2, statements.size()); + } + + @Test + public void testMySqlDropObjectTypes() throws JSQLParserException { + Drop database = (Drop) assertSqlCanBeParsedAndDeparsed( + "DROP DATABASE IF EXISTS database_1"); + assertEquals(Drop.ObjectType.DATABASE, database.getObjectType()); + + Drop procedure = (Drop) assertSqlCanBeParsedAndDeparsed( + "DROP PROCEDURE IF EXISTS procedure_1"); + assertEquals(Drop.ObjectType.PROCEDURE, procedure.getObjectType()); + + Drop trigger = (Drop) assertSqlCanBeParsedAndDeparsed( + "DROP TRIGGER IF EXISTS schema_1.trigger_1"); + assertEquals(Drop.ObjectType.TRIGGER, trigger.getObjectType()); + assertEquals("schema_1.trigger_1", trigger.getName().getFullyQualifiedName()); + } + + @Test + public void testMySqlDropMultipleTables() throws JSQLParserException { + String sql = "DROP TABLE IF EXISTS table_1, table_2, table_3 RESTRICT"; + Drop drop = (Drop) assertSqlCanBeParsedAndDeparsed(sql); + + assertEquals(List.of("table_1", "table_2", "table_3"), drop.getNames().stream() + .map(Table::getFullyQualifiedName).collect(Collectors.toList())); + assertEquals("table_1", drop.getName().getFullyQualifiedName()); + assertEquals(List.of("RESTRICT"), drop.getParameters()); + + assertDeparse(new Drop().withObjectType(Drop.ObjectType.TABLE).withIfExists(true) + .withNames(List.of(new Table("table_1"), new Table("table_2"), + new Table("table_3"))) + .addParameters("RESTRICT"), sql); + } + + @Test + public void testMySqlDropMultipleViews() throws JSQLParserException { + Drop drop = (Drop) assertSqlCanBeParsedAndDeparsed( + "DROP VIEW IF EXISTS view_1, view_2 CASCADE"); + + assertEquals(Drop.ObjectType.VIEW, drop.getObjectType()); + assertEquals(List.of("view_1", "view_2"), drop.getNames().stream() + .map(Table::getFullyQualifiedName).collect(Collectors.toList())); + assertEquals(List.of("CASCADE"), drop.getParameters()); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/OracleDropSynonymTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/OracleDropSynonymTest.java new file mode 100644 index 0000000000..237ed6671f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/drop/OracleDropSynonymTest.java @@ -0,0 +1,109 @@ +/*- + * #%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.statement.drop; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class OracleDropSynonymTest { + @ParameterizedTest + @ValueSource(strings = {"sample_syn", "\"Sample.Synonym\""}) + void preservesSynonymModifiers(String name) throws JSQLParserException { + for (String scope : new String[] {"", "PUBLIC "}) { + for (String existence : new String[] {"", "IF EXISTS "}) { + for (String force : new String[] {"", " FORCE"}) { + String sql = "DROP " + scope + "SYNONYM " + existence + name + force; + for (Dialect dialect : new Dialect[] {null, Dialect.ORACLE}) { + Drop drop = parse(sql, dialect); + assertEquals(Drop.ObjectType.SYNONYM, drop.getObjectType()); + assertEquals("SYNONYM", drop.getType()); + assertEquals(!scope.isEmpty(), drop.isPublicSynonym()); + assertEquals(!existence.isEmpty(), drop.isIfExists()); + assertEquals(!force.isEmpty(), drop.isForce()); + assertEquals(1, drop.getNames().size()); + assertEquals(name, drop.getName().getName()); + assertEquals(sql, drop.toString()); + roundTrip(drop, dialect); + assertEquals(2, + CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + } + } + } + } + } + + @Test + void preservesQualifiedNamesAndSupportsModelChanges() throws JSQLParserException { + Drop drop = parse("DROP SYNONYM IF EXISTS \"hr.schema\".\"sample.syn\" FORCE", + Dialect.ORACLE); + assertEquals("\"hr.schema\"", drop.getName().getSchemaName()); + assertEquals("\"sample.syn\"", drop.getName().getName()); + drop.getName().setName("replacement"); + assertEquals("DROP SYNONYM IF EXISTS \"hr.schema\".replacement FORCE", drop.toString()); + roundTrip(drop, Dialect.ORACLE); + + Drop created = new Drop().withObjectType(Drop.ObjectType.SYNONYM) + .withPublicSynonym(true).withIfExists(true).withName(new Table("s")) + .withForce(true); + assertEquals("DROP PUBLIC SYNONYM IF EXISTS s FORCE", created.toString()); + roundTrip(created, Dialect.ORACLE); + created.setPublicSynonym(false); + created.setForce(false); + assertEquals("DROP SYNONYM IF EXISTS s", created.toString()); + roundTrip(created, null); + } + + @Test + void keepsPostgreSqlForceSyntax() throws JSQLParserException { + Drop database = parse("DROP DATABASE d WITH (FORCE)", Dialect.POSTGRESQL); + assertTrue(database.isForce()); + assertEquals("DROP DATABASE d WITH (FORCE)", database.toString()); + roundTrip(database, Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP PUBLIC TABLE t", "DROP SYNONYM", + "DROP PUBLIC SYNONYM IF EXISTS", + "DROP SYNONYM s FORCE FORCE", "DROP SYNONYM s WITH (FORCE)", + "DROP TABLE t FORCE"}) + void rejectsMalformedSynonymClauses(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.ORACLE)); + } + + private static Drop parse(String sql, Dialect dialect) throws JSQLParserException { + return (Drop) CCJSqlParserUtil.parse(sql, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + } + + private static void roundTrip(Drop drop, Dialect dialect) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + drop.accept(new StatementDeParser(output), null); + assertEquals(drop.toString(), output.toString()); + Drop reparsed = parse(output.toString(), dialect); + assertEquals(drop.getObjectType(), reparsed.getObjectType()); + assertEquals(drop.isPublicSynonym(), reparsed.isPublicSynonym()); + assertEquals(drop.isIfExists(), reparsed.isIfExists()); + assertEquals(drop.isForce(), reparsed.isForce()); + assertEquals(output.toString(), reparsed.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/PostgreSqlDropIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/PostgreSqlDropIndexTest.java new file mode 100644 index 0000000000..e0fd692a37 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/drop/PostgreSqlDropIndexTest.java @@ -0,0 +1,56 @@ +/*- + * #%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.statement.drop; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlDropIndexTest { + @ParameterizedTest + @ValueSource(strings = {"DROP INDEX CONCURRENTLY ix", + "DROP INDEX CONCURRENTLY IF EXISTS ix", + "DROP INDEX CONCURRENTLY IF EXISTS public.ix RESTRICT"}) + void concurrencyIsIndependentOfTarget(String sql) throws JSQLParserException { + Drop drop = (Drop) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + assertTrue(drop.isConcurrently()); + assertEquals(sql.contains("IF EXISTS"), drop.isIfExists()); + assertEquals("ix", drop.getName().getName()); + assertEquals(sql, drop.toString()); + drop.setName(new Table("new_ix")); + String expected = sql.replace("public.ix", "ix").replace("ix", "new_ix"); + assertEquals(expected, drop.toString()); + StringBuilder buffer = new StringBuilder(); + drop.accept(new StatementDeParser(buffer), null); + assertEquals(expected, buffer.toString()); + Drop reparsed = (Drop) CCJSqlParserUtil.parse(expected); + assertEquals("new_ix", reparsed.getName().getName()); + assertTrue(reparsed.isConcurrently()); + assertEquals(2, CCJSqlParserUtil.parseStatements(expected + "; SELECT 1").size()); + drop.setConcurrently(false); + assertEquals(expected.replace("CONCURRENTLY ", ""), drop.toString()); + } + + @Test + void ordinaryAndQuotedConcurrentNamesArePreserved() throws JSQLParserException { + for (String sql : new String[] {"DROP TABLE concurrently", "DROP INDEX \"concurrently\"", + "DROP INDEX ix1, ix2 CASCADE", "DROP INDEX ix ON t ALGORITHM = INPLACE"}) { + Drop drop = (Drop) CCJSqlParserUtil.parse(sql); + assertFalse(drop.isConcurrently()); + assertEquals(sql, drop.toString()); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/execute/ExecuteArgumentsTest.java b/src/test/java/net/sf/jsqlparser/statement/execute/ExecuteArgumentsTest.java new file mode 100644 index 0000000000..c303d436f9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/execute/ExecuteArgumentsTest.java @@ -0,0 +1,140 @@ +/*- + * #%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.statement.execute; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.JdbcNamedParameter; +import net.sf.jsqlparser.expression.UserVariable; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ExecuteArgumentsTest { + private Execute roundtrip(String sql) throws JSQLParserException { + Execute execute = (Execute) CCJSqlParserUtil.parse(sql, + p -> p.withSquareBracketQuotation(true)); + StringBuilder text = new StringBuilder(); + execute.accept(new StatementDeParser(text), null); + assertThat(text.toString()).isEqualTo(execute.toString()); + assertThat(CCJSqlParserUtil.parse(text.toString(), p -> p.withSquareBracketQuotation(true)) + .toString()).isEqualTo(execute.toString()); + return execute; + } + + @ParameterizedTest + @ValueSource(strings = {"EXEC", "EXECUTE", "CALL"}) + void namedBindsRemainArguments(String command) throws JSQLParserException { + for (String name : List.of("PCK_ACTION_BY", "dbo.p", "db..p", "srv.db.dbo.p", + "\"a.b\".\"p\"", "[dbo].[p]")) { + Execute execute = roundtrip(command + " " + name + " :USER_ID, :GROUP_ID"); + assertThat(execute.getName()).isEqualTo(name); + assertThat(execute.getExprList()).hasSize(2); + assertThat(execute.getExprList().get(0)).isInstanceOf(JdbcNamedParameter.class); + assertThat(((JdbcNamedParameter) execute.getExprList().get(0)).getName()) + .isEqualTo("USER_ID"); + } + } + + @ParameterizedTest + @ValueSource(strings = {"EXECUTE myProc 'foo', @outputVar OUTPUT", + "EXEC p @result = @value OUTPUT, :other", "EXEC p @value OUT", + "EXEC p :value OUTPUT", "EXEC p ? OUTPUT"}) + void outputArgumentsHaveTheirOwnModel(String sql) throws JSQLParserException { + Execute execute = roundtrip(sql); + assertThat(execute.getExprList()).anySatisfy(expression -> { + assertThat(expression).isInstanceOf(ExecuteArgument.class); + assertThat(((ExecuteArgument) expression).isOutput()).isTrue(); + }); + assertThat(Validation.validate(List.of(new FeaturesAllowed(Feature.values())), sql)) + .isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"CALL p ()", "CALL p (:a, :b)", "EXEC p (1 + 2) * 3", + "EXEC p @param = 'foo', @param2 = 'bar'", "CALL dbo.p", "EXEC p -1, NULL"}) + void legacyArgumentFormsStillRoundtrip(String sql) throws JSQLParserException { + roundtrip(sql); + } + + @Test + void preservesFollowingStatementsAndVisitsOutputVariables() throws JSQLParserException { + Statements statements = CCJSqlParserUtil.parseStatements( + "EXEC p @a OUTPUT, @b = @c OUTPUT; SELECT 42;"); + assertThat(statements).hasSize(2); + assertThat(statements.get(1).toString()).isEqualTo("SELECT 42"); + Execute execute = (Execute) statements.get(0); + List variables = new ArrayList<>(); + Object marker = new Object(); + execute.getExprList().accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(UserVariable variable, S context) { + assertThat(context).isSameAs(marker); + variables.add(variable.getName()); + return null; + } + }, marker); + assertThat(variables).containsExactly("a", "b", "c"); + variables.clear(); + execute.getExprList().accept(new net.sf.jsqlparser.util.TablesNamesFinder() { + @Override + public Void visit(UserVariable variable, S context) { + assertThat(context).isSameAs(marker); + variables.add(variable.getName()); + return null; + } + }, marker); + assertThat(variables).containsExactly("a", "b", "c"); + } + + @Test + void customDeparserRetainsTheOutputModifierAndLegacyMutation() throws JSQLParserException { + Execute execute = roundtrip("EXEC p @a OUTPUT"); + StringBuilder text = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(UserVariable variable, S context) { + getBuilder().append('@').append(variable.getName()).append("_rewritten"); + return getBuilder(); + } + }; + expressions.setBuilder(text); + new net.sf.jsqlparser.util.deparser.ExecuteDeParser(expressions, text).deParse(execute); + assertThat(text.toString()).isEqualTo("EXEC p @a_rewritten OUTPUT"); + + ExecuteArgument argument = (ExecuteArgument) execute.getExprList().get(0); + argument.setOutput(false); + argument.setExpression(new JdbcNamedParameter().withName("replacement")); + execute.setExprList(new ExpressionList<>(argument)); + execute.setName(Arrays.asList("db", null, "renamed")); + assertThat(execute.toString()).isEqualTo("EXEC db..renamed :replacement"); + } + + @ParameterizedTest + @ValueSource(strings = {"EXEC p 1 OUTPUT", "EXEC p 'x' OUT", "EXEC p @x = 1 OUTPUT", + "EXEC p @x OUTPUT OUTPUT", "EXEC p :id,"}) + void rejectsMalformedOutputArguments(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/grant/GranteeModelTest.java b/src/test/java/net/sf/jsqlparser/statement/grant/GranteeModelTest.java new file mode 100644 index 0000000000..dd26645eed --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/grant/GranteeModelTest.java @@ -0,0 +1,195 @@ +/*- + * #%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.statement.grant; + +import static net.sf.jsqlparser.test.TestUtils.assertEqualsObjectTree; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.Collections; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** The grantee model shared by MySQL and PostgreSQL, and the string-based legacy API. */ +class GranteeModelTest { + + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + private static Statement parse(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), POSTGRESQL).toString()); + return statement; + } + + @Test + void typesPostgreSqlRoleSpecifications() throws JSQLParserException { + Grant grant = (Grant) parse("GRANT SELECT ON users TO app, GROUP staff, PUBLIC, " + + "CURRENT_USER, CURRENT_ROLE, SESSION_USER, \"Mixed Case\" GRANTED BY CURRENT_ROLE"); + + assertThat(grant.getClause().getGranteeItems()).extracting(Grantee::getKind) + .containsExactly(Grantee.Kind.ACCOUNT, Grantee.Kind.GROUP, Grantee.Kind.PUBLIC, + Grantee.Kind.CURRENT_USER, Grantee.Kind.CURRENT_ROLE, + Grantee.Kind.SESSION_USER, Grantee.Kind.ACCOUNT); + Grantee group = grant.getClause().getGranteeItems().get(1); + assertEquals("staff", group.getAccount().getUser().getValue()); + assertNull(group.getAccount().getHost()); + Grantee quoted = grant.getClause().getGranteeItems().get(6); + assertEquals("Mixed Case", quoted.getAccount().getUser().getValue()); + assertEquals("\"", quoted.getAccount().getUser().getQuoteStr()); + assertEquals(Grantee.Kind.CURRENT_ROLE, grant.getClause().getGrantor().getKind()); + + assertEquals(Arrays.asList("app", "GROUP staff", "PUBLIC", "CURRENT_USER", "CURRENT_ROLE", + "SESSION_USER", "\"Mixed Case\""), grant.getUsers()); + assertEquals("CURRENT_ROLE", grant.getClause().getGrantedBy()); + assertThat(TablesNamesFinder.findTables(grant.toString())).containsExactly("users"); + } + + @Test + void typesMembershipOptionList() throws JSQLParserException { + Grant grant = (Grant) parse( + "GRANT app_read TO alice WITH INHERIT TRUE, SET FALSE, ADMIN OPTION GRANTED BY postgres"); + + assertNull(grant.getTarget()); + assertThat(grant.getOptions()).extracting(GrantOption::getKind).containsExactly( + GrantOption.Kind.INHERIT, GrantOption.Kind.SET, GrantOption.Kind.ADMIN); + assertThat(grant.getOptions()).extracting(GrantOption::getValue).containsExactly( + GrantOption.Value.TRUE, GrantOption.Value.FALSE, GrantOption.Value.OPTION); + assertSame(grant.getOptions().get(0), grant.getOption()); + assertEquals("postgres", grant.getClause().getGrantor().getAccount().getUser().getValue()); + + grant.setOption(new GrantOption(GrantOption.Kind.ADMIN)); + assertEquals("GRANT app_read TO alice WITH ADMIN OPTION GRANTED BY postgres", + grant.toString()); + grant.setOption(null); + assertEquals("GRANT app_read TO alice GRANTED BY postgres", grant.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "GRANT app_read TO alice WITH INHERIT TRUE, INHERIT FALSE", + "GRANT app_read TO alice WITH ADMIN OPTION, GRANT OPTION", + "GRANT SELECT ON users TO alice WITH GRANT OPTION, INHERIT TRUE", + "GRANT SELECT ON users TO alice GRANTED BY PUBLIC", + "GRANT SELECT ON users TO alice GRANTED BY GROUP staff", + "GRANT SELECT ON users TO GROUP" + }) + void rejectsInvalidRoleSpecifications(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, POSTGRESQL)); + } + + @Test + void legacyGranteeStringsWriteThroughTypedState() throws JSQLParserException { + Grant grant = (Grant) CCJSqlParserUtil.parse("GRANT SELECT ON t TO 'app'@'%', app2", + parser -> parser.withDialect(Dialect.MYSQL)); + Grantee typed = grant.getClause().getGranteeItems().get(0); + + grant.getUsers().add("PUBLIC"); + grant.getUsers().set(1, "GROUP staff"); + grant.addUsers("CURRENT_USER()"); + assertSame(typed, grant.getClause().getGranteeItems().get(0)); + assertThat(grant.getClause().getGranteeItems()).extracting(Grantee::getKind) + .containsExactly(Grantee.Kind.ACCOUNT, Grantee.Kind.GROUP, Grantee.Kind.PUBLIC, + Grantee.Kind.CURRENT_USER); + assertEquals("GRANT SELECT ON t TO 'app'@'%', GROUP staff, PUBLIC, CURRENT_USER()", + grant.toString()); + + grant.getUsers().remove(0); + grant.getClause().setGrantedBy("SESSION_USER"); + assertEquals(Grantee.Kind.SESSION_USER, grant.getClause().getGrantor().getKind()); + assertEquals("GRANT SELECT ON t TO GROUP staff, PUBLIC, CURRENT_USER() " + + "GRANTED BY SESSION_USER", grant.toString()); + + grant.setUsers(Collections.singletonList("reader")); + assertEquals(Grantee.Kind.ACCOUNT, grant.getClause().getGranteeItems().get(0).getKind()); + grant.setUsers(null); + assertNull(grant.getUsers()); + assertNull(new Grant().getUsers()); + } + + @Test + void legacyBuildersMatchParsedStatements() throws JSQLParserException { + Grant parsed = + (Grant) CCJSqlParserUtil.parse("GRANT SELECT, CREATE VIEW ON t1 TO u, PUBLIC"); + Grant built = new Grant().addPrivileges("SELECT", "CREATE VIEW").withObjectName("t1") + .addUsers("u", "PUBLIC"); + assertEqualsObjectTree(parsed, built); + assertEquals(Privilege.Kind.CREATE_VIEW, built.getPrivilegeItems().get(1).getKind()); + + Grant role = (Grant) CCJSqlParserUtil.parse("GRANT r1 TO u GRANTED BY CURRENT_USER"); + Grant builtRole = new Grant().withRole("r1").addUsers("u"); + builtRole.getClause().setGrantedBy("CURRENT_USER"); + assertEqualsObjectTree(role, builtRole); + assertNull(builtRole.getTarget()); + + builtRole.getRoles().add("'r2'@'%'"); + assertEquals("GRANT r1, 'r2'@'%' TO u GRANTED BY CURRENT_USER", builtRole.toString()); + assertEquals("r1", builtRole.getRole()); + } + + @Test + void legacyObjectNameReplacesWildcard() throws JSQLParserException { + Grant grant = (Grant) CCJSqlParserUtil.parse("GRANT SELECT ON *.* TO app"); + assertEquals("*.*", grant.getObjectName()); + + grant.setObjectName(Arrays.asList("db1", "t1")); + assertNull(grant.getTarget().getWildcard()); + assertEquals("GRANT SELECT ON db1.t1 TO app", grant.toString()); + + Grant role = (Grant) CCJSqlParserUtil.parse("GRANT reader TO app"); + assertNull(role.getObjectName()); + role.setPrivileges(Collections.singletonList("SELECT")); + role.setObjectName("t1"); + assertEquals("GRANT SELECT ON t1 TO app", role.toString()); + } + + @Test + void legacyPrivilegeNames() { + assertEquals(Privilege.Kind.CREATE_TEMPORARY_TABLES, + new Privilege("CREATE TEMPORARY TABLES").getKind()); + assertEquals(Privilege.Kind.ALTER_SYSTEM, new Privilege("ALTER SYSTEM").getKind()); + Privilege unknown = new Privilege("DYNAMIC"); + assertNull(unknown.getKind()); + assertEquals("DYNAMIC", unknown.toString()); + Privilege text = new Privilege("select (a)"); + assertNull(text.getKind()); + assertEquals("select (a)", text.getName()); + assertFalse(new Privilege(Privilege.Kind.SELECT).isUsePrivileges()); + } + + @Test + void grantedByAndRolesAcceptLegacyText() { + PrivilegeClause clause = new PrivilegeClause(); + clause.setGrantedBy("GROUP admins"); + assertEquals(Grantee.Kind.GROUP, clause.getGrantor().getKind()); + clause.setGrantedBy(null); + assertNull(clause.getGrantor()); + + clause.setRoles(Arrays.asList("a", "b")); + clause.getRoles().set(0, "c"); + assertEquals(Arrays.asList("c", "b"), clause.getRoles()); + clause.setRoles(null); + assertEquals(Collections.emptyList(), clause.getRoleItems()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/grant/MySqlGrantRevokeTest.java b/src/test/java/net/sf/jsqlparser/statement/grant/MySqlGrantRevokeTest.java new file mode 100644 index 0000000000..9e1009ef2c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/grant/MySqlGrantRevokeTest.java @@ -0,0 +1,355 @@ +/*- + * #%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.statement.grant; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.user.RoleSelection; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlGrantRevokeTest { + + private static final Consumer MYSQL = parser -> parser.withDialect(Dialect.MYSQL); + + /** Parses with the MySQL dialect, checks toString and the deparser, and reparses. */ + private static Statement parse(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, MYSQL); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), MYSQL).toString()); + return statement; + } + + private static Grant grant(String sql) throws JSQLParserException { + return (Grant) parse(sql); + } + + private static Revoke revoke(String sql) throws JSQLParserException { + return (Revoke) parse(sql); + } + + /** The value and quote of the user and host, for example {@code app(')@%(')}. */ + private static String account(UserAccount account) { + StringValue host = account.getHost(); + return account.getUser().getValue() + "(" + account.getUser().getQuoteStr() + ")" + + (host == null ? "" : "@" + host.getValue() + "(" + host.getQuoteStr() + ")"); + } + + @ParameterizedTest + @ValueSource(strings = { + "GRANT SELECT ON db1.* TO 'app'@'%'", + "GRANT SELECT, INSERT, UPDATE, DELETE ON `db1`.`t1` TO 'app'@'%' WITH GRANT OPTION", + "GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION", + "GRANT SELECT (id, name), UPDATE (name) ON db1.t1 TO 'app'@'%'", + "GRANT EXECUTE ON PROCEDURE db1.p1 TO 'app'@'%'", + "GRANT EXECUTE ON FUNCTION db1.f1 TO app@'%'", + "GRANT CREATE, ALTER, DROP, INDEX, REFERENCES, CREATE VIEW, SHOW VIEW, TRIGGER, EVENT, " + + "CREATE ROUTINE, ALTER ROUTINE, CREATE TEMPORARY TABLES, LOCK TABLES " + + "ON db1.* TO 'dev'@'%'", + "GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT " + + "ON *.* TO 'cdc'@'%'", + "GRANT PROCESS, SUPER, FILE, SHUTDOWN, CREATE USER, CREATE TABLESPACE, CREATE ROLE, " + + "DROP ROLE, USAGE, GRANT OPTION ON *.* TO 'admin'@'%'", + "GRANT BACKUP_ADMIN, replication_slave_admin ON *.* TO 'admin'@'%'", + "GRANT 'reader', 'writer'@'%' TO 'app'@'%' WITH ADMIN OPTION", + "GRANT reader TO app", + "GRANT PROXY ON 'a'@'%' TO 'b'@'%', CURRENT_USER WITH GRANT OPTION", + "GRANT SELECT ON TABLE db1.* TO `app`@`%`", + "GRANT ALL ON * TO app@localhost", + "GRANT SELECT ON `my-db`.* TO 'app'@localhost, CURRENT_USER()", + "GRANT SELECT ON db1.* TO 'app'@'%' AS 'admin'@'%' WITH ROLE 'r1', r2@'%'", + "GRANT SELECT ON db1.t1 TO app WITH GRANT OPTION AS CURRENT_USER WITH ROLE DEFAULT", + "REVOKE INSERT ON db1.* FROM 'app'@'%', app2", + "REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app'@'%'", + "REVOKE ALL, GRANT OPTION FROM app", + "REVOKE GRANT OPTION ON *.* FROM app", + "REVOKE 'reader', writer FROM 'app'@'%'", + "REVOKE IF EXISTS SELECT ON db1.t1 FROM 'app'@'%' IGNORE UNKNOWN USER", + "REVOKE IF EXISTS PROXY ON 'a'@'%' FROM 'b'@'%'", + "REVOKE REPLICATION SLAVE, REPLICATION CLIENT ON *.* FROM 'repl'@'%'" + }) + void roundTripsMySqlForms(String sql) throws JSQLParserException { + Statement statement = parse(sql); + + Statement withoutDialect = assertSqlCanBeParsedAndDeparsed(sql, false); + assertEquals(statement.toString(), withoutDialect.toString()); + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "* | DEFAULT_DATABASE | [] | *", + "*.* | GLOBAL | [] | *.*", + "db1.* | DATABASE | [db1] | db1.*", + "`my-db`.* | DATABASE | [`my-db`] | `my-db`.*" + }) + void typesWildcardPrivilegeLevels(String level, PrivilegeTarget.Wildcard wildcard, + String names, String objectName) throws JSQLParserException { + Grant grant = grant("GRANT SELECT ON " + level + " TO app"); + PrivilegeTarget target = grant.getTarget(); + + assertEquals(wildcard, target.getWildcard()); + assertEquals(PrivilegeTarget.Kind.TABLE, target.getKind()); + assertFalse(target.isExplicitKind()); + assertEquals(names, target.getNames().stream().map(name -> name.get(0)) + .collect(Collectors.toList()).toString()); + assertEquals(objectName, grant.getObjectName()); + assertThat(TablesNamesFinder.findTables(grant.toString())).isEmpty(); + } + + @Test + void namedPrivilegeLevelsRemainTables() throws JSQLParserException { + Grant qualified = grant("GRANT SELECT ON TABLE db1.t1 TO app"); + assertNull(qualified.getTarget().getWildcard()); + assertTrue(qualified.getTarget().isExplicitKind()); + assertEquals(Arrays.asList("db1", "t1"), qualified.getTarget().getNames().get(0)); + assertThat(TablesNamesFinder.findTables(qualified.toString())).containsExactly("db1.t1"); + + Grant routine = grant("GRANT EXECUTE ON PROCEDURE db1.p1 TO app"); + assertEquals(PrivilegeTarget.Kind.PROCEDURE, routine.getTarget().getKind()); + assertEquals("db1.p1", routine.getTarget().getRoutines().get(0).getName()); + } + + @Test + void typesStaticAndDynamicPrivileges() throws JSQLParserException { + Grant grant = grant("GRANT CREATE TEMPORARY TABLES, LOCK TABLES, SHOW DATABASES, " + + "REPLICATION SLAVE, REPLICATION CLIENT, ALTER ROUTINE, CREATE VIEW, INDEX, " + + "SHUTDOWN, GRANT OPTION, backup_admin ON *.* TO app"); + + assertThat(grant.getPrivilegeItems()).extracting(Privilege::getKind).containsExactly( + Privilege.Kind.CREATE_TEMPORARY_TABLES, Privilege.Kind.LOCK_TABLES, + Privilege.Kind.SHOW_DATABASES, Privilege.Kind.REPLICATION_SLAVE, + Privilege.Kind.REPLICATION_CLIENT, Privilege.Kind.ALTER_ROUTINE, + Privilege.Kind.CREATE_VIEW, Privilege.Kind.INDEX, Privilege.Kind.SHUTDOWN, + Privilege.Kind.GRANT_OPTION, Privilege.Kind.DYNAMIC); + Privilege dynamic = grant.getPrivilegeItems().get(10); + assertEquals("backup_admin", dynamic.getDynamicName()); + assertEquals("backup_admin", dynamic.getName()); + assertEquals("CREATE TEMPORARY TABLES", grant.getPrivilegeItems().get(0).getName()); + assertEquals("CREATE TEMPORARY TABLES", grant.getPrivileges().get(0)); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', quoteCharacter = '"', value = { + "'app'@'%' | app(')@%(')", + "app@'10.0.0.%' | app()@10.0.0.%(')", + "app@localhost | app()@localhost()", + "'app'@localhost | app(')@localhost()", + "`app`@`%` | app(`)@%(`)", + "'app' | app(')", + "app | app()" + }) + void typesAccountGrantees(String account, String expected) throws JSQLParserException { + Grant grant = grant("GRANT SELECT ON db1.t1 TO " + account); + Grantee grantee = grant.getClause().getGranteeItems().get(0); + + assertEquals(Grantee.Kind.ACCOUNT, grantee.getKind()); + assertEquals(expected, account(grantee.getAccount())); + assertEquals(Collections.singletonList(account), grant.getUsers()); + } + + @Test + void typesCurrentUser() throws JSQLParserException { + Grant grant = grant("GRANT SELECT ON db1.* TO CURRENT_USER, CURRENT_USER()"); + List grantees = grant.getClause().getGranteeItems(); + + assertThat(grantees).extracting(Grantee::getKind) + .containsExactly(Grantee.Kind.CURRENT_USER, Grantee.Kind.CURRENT_USER); + assertFalse(grantees.get(0).isCurrentUserParentheses()); + assertTrue(grantees.get(1).isCurrentUserParentheses()); + assertNull(grantees.get(0).getAccount()); + assertEquals(Arrays.asList("CURRENT_USER", "CURRENT_USER()"), grant.getUsers()); + } + + @Test + void roleGrantsHaveTypedRolesAndNoTarget() throws JSQLParserException { + Grant grant = grant("GRANT 'reader', 'writer'@'%' TO 'app'@'%' WITH ADMIN OPTION"); + + assertNull(grant.getTarget()); + assertNull(grant.getPrivilegeItems()); + assertNull(grant.getObjectName()); + assertThat(grant.getClause().getRoleItems()).extracting(MySqlGrantRevokeTest::account) + .containsExactly("reader(')", "writer(')@%(')"); + assertEquals(Arrays.asList("'reader'", "'writer'@'%'"), grant.getRoles()); + assertEquals("'reader'", grant.getRole()); + assertEquals(GrantOption.Kind.ADMIN, grant.getOption().getKind()); + assertThat(TablesNamesFinder.findTables(grant.toString())).isEmpty(); + + Revoke revoke = revoke("REVOKE 'reader' FROM 'app'@'%'"); + assertNull(revoke.getClause().getTarget()); + assertEquals("reader", revoke.getClause().getRoleItems().get(0).getUser().getValue()); + } + + @Test + void proxyTargetsAnAccount() throws JSQLParserException { + Grant grant = grant("GRANT PROXY ON 'a'@'%' TO 'b'@'%' WITH GRANT OPTION"); + + assertThat(grant.getPrivilegeItems()).extracting(Privilege::getKind) + .containsExactly(Privilege.Kind.PROXY); + PrivilegeTarget target = grant.getTarget(); + assertEquals(PrivilegeTarget.Kind.ACCOUNT, target.getKind()); + assertEquals("a(')@%(')", account(target.getAccount().getAccount())); + assertTrue(target.getNames().isEmpty()); + assertNull(grant.getObjectName()); + assertThat(TablesNamesFinder.findTables(grant.toString())).isEmpty(); + + Revoke revoke = revoke("REVOKE PROXY ON CURRENT_USER FROM 'b'@'%'"); + assertEquals(Grantee.Kind.CURRENT_USER, + revoke.getClause().getTarget().getAccount().getKind()); + } + + @Test + void revokesAllPrivilegesAndGrantOptionWithoutTarget() throws JSQLParserException { + Revoke revoke = revoke("REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app'@'%', app2"); + PrivilegeClause clause = revoke.getClause(); + + assertThat(clause.getPrivileges()).extracting(Privilege::getKind) + .containsExactly(Privilege.Kind.ALL, Privilege.Kind.GRANT_OPTION); + assertTrue(clause.getPrivileges().get(0).isUsePrivileges()); + assertNull(clause.getTarget()); + assertEquals(Arrays.asList("'app'@'%'", "app2"), clause.getGrantees()); + assertThat(TablesNamesFinder.findTables(revoke.toString())).isEmpty(); + } + + @Test + void revokeIfExistsIgnoreUnknownUser() throws JSQLParserException { + Revoke revoke = + revoke("REVOKE IF EXISTS SELECT ON db1.t1 FROM 'app'@'%' IGNORE UNKNOWN USER"); + assertTrue(revoke.isIfExists()); + assertTrue(revoke.isIgnoreUnknownUser()); + assertThat(TablesNamesFinder.findTables(revoke.toString())).containsExactly("db1.t1"); + + revoke.setIfExists(false); + revoke.setIgnoreUnknownUser(false); + assertEquals("REVOKE SELECT ON db1.t1 FROM 'app'@'%'", revoke.toString()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', quoteCharacter = '"', value = { + "DEFAULT | DEFAULT | \"\"", + "NONE | NONE | \"\"", + "ALL | ALL | \"\"", + "ALL EXCEPT 'r1', r2@'%' | ALL_EXCEPT | 'r1', r2@'%'", + "'r1', r2 | ROLES | 'r1', r2" + }) + void typesAsUserWithRole(String selection, RoleSelection.Kind kind, String roles) + throws JSQLParserException { + Grant grant = grant("GRANT SELECT ON db1.* TO app AS 'admin'@'%' WITH ROLE " + selection); + + assertEquals(Grantee.Kind.ACCOUNT, grant.getAsUser().getKind()); + assertEquals("admin(')@%(')", account(grant.getAsUser().getAccount())); + assertEquals(kind, grant.getWithRole().getKind()); + assertEquals(roles, grant.getWithRole().getRoles().stream().map(Object::toString) + .collect(Collectors.joining(", "))); + } + + @ParameterizedTest + @ValueSource(strings = { + "GRANT SELECT ON db1.t1.* TO app", + "GRANT SELECT ON db1.*, db2.* TO app", + "GRANT USAGE ON SCHEMA * TO app", + "GRANT EXECUTE ON PROCEDURE db1.* TO app", + "GRANT PROXY ON db1.t1 TO app", + "GRANT PROXY, SELECT ON t1 TO app", + "GRANT PROXY ON PUBLIC TO app", + "GRANT PROXY ON 'a'@'%' TO app AS admin", + "GRANT reader TO app AS admin", + "GRANT SELECT ON t1 TO app AS PUBLIC", + "GRANT SELECT ON t1 TO app WITH ROLE r1", + "GRANT SELECT ON t1 TO app WITH GRANT OPTION, GRANT OPTION", + "GRANT SELECT ON t1 TO 'app'@", + "GRANT SELECT ON t1 TO app@", + "GRANT SELECT ON t1 TO PUBLIC()", + "GRANT BACKUP_ADMIN PRIVILEGES ON *.* TO app", + "GRANT ALL PRIVILEGES, GRANT OPTION TO app", + "REVOKE SELECT ON t1 FROM app IGNORE UNKNOWN", + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON *.* TO app", + "ALTER DEFAULT PRIVILEGES REVOKE IF EXISTS SELECT ON TABLES FROM app", + "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM app IGNORE UNKNOWN USER", + "ALTER DEFAULT PRIVILEGES GRANT PROXY ON a TO app", + "ALTER DEFAULT PRIVILEGES REVOKE ALL, GRANT OPTION FROM app" + }) + void rejectsInvalidForms(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL)); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @Test + void buildsTypedGrant() { + PrivilegeTarget target = new PrivilegeTarget(); + target.setWildcard(PrivilegeTarget.Wildcard.DATABASE); + target.getNames().add(Collections.singletonList("db1")); + UserAccount app = new UserAccount().withUser(new StringValue("app")) + .withHost(new StringValue("%")); + Grant grant = new Grant(); + grant.setPrivilegeItems(Arrays.asList(new Privilege(Privilege.Kind.SHOW_VIEW), + Privilege.dynamic("BACKUP_ADMIN"))); + grant.setTarget(target); + grant.getClause().setGranteeItems(Arrays.asList(new Grantee(app), + new Grantee(Grantee.Kind.CURRENT_USER).withCurrentUserParentheses(true))); + grant.setOption(new GrantOption(GrantOption.Kind.GRANT)); + grant.setAsUser(new Grantee(new UserAccount().withUser(new StringValue("admin")))); + grant.setWithRole(new RoleSelection(RoleSelection.Kind.ALL_EXCEPT) + .withRoles(Collections.singletonList( + new UserAccount().withUser(new StringValue("r1"))))); + + assertDeparse(grant, "GRANT SHOW VIEW, BACKUP_ADMIN ON db1.* TO 'app'@'%', " + + "CURRENT_USER() WITH GRANT OPTION AS 'admin' WITH ROLE ALL EXCEPT 'r1'"); + + Revoke revoke = new Revoke(); + revoke.setIfExists(true); + revoke.getClause().setPrivileges(Collections.singletonList( + new Privilege(Privilege.Kind.PROXY))); + PrivilegeTarget proxied = new PrivilegeTarget(); + proxied.setKind(PrivilegeTarget.Kind.ACCOUNT); + proxied.setAccount(new Grantee(app)); + revoke.getClause().setTarget(proxied); + revoke.getClause().setGranteeItems( + Collections.singletonList(new Grantee(Grantee.Kind.CURRENT_USER))); + revoke.setIgnoreUnknownUser(true); + assertDeparse(revoke, + "REVOKE IF EXISTS PROXY ON 'app'@'%' FROM CURRENT_USER IGNORE UNKNOWN USER"); + } + + @Test + void validatesWithMySqlAndMariaDbCapabilities() { + for (String sql : Arrays.asList("GRANT SELECT ON db1.* TO 'app'@'%'", + "GRANT 'reader' TO 'app'@'%'", + "REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app'@'%'", + "REVOKE SELECT ON *.* FROM app")) { + validateNoErrors(sql, 1, DatabaseType.MYSQL, DatabaseType.MARIADB); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/insert/InsertBulkTest.java b/src/test/java/net/sf/jsqlparser/statement/insert/InsertBulkTest.java new file mode 100644 index 0000000000..bf764fd9f2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/insert/InsertBulkTest.java @@ -0,0 +1,151 @@ +/*- + * #%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.statement.insert; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.SqlServerVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class InsertBulkTest { + private static InsertBulk parse(String sql) throws Exception { + return (InsertBulk) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.SQLSERVER)); + } + + @Test + void parsesIssue2033() throws Exception { + String sql = "INSERT BULK tpch.dbo.order_line ([ol_o_id] int, [ol_d_id] tinyint, " + + "[ol_w_id] int, [ol_number] tinyint, [ol_i_id] int, [ol_delivery_d] datetime, " + + "[ol_amount] smallmoney, [ol_supply_w_id] int, [ol_quantity] smallint, " + + "[ol_dist_info] char(24) COLLATE Chinese_PRC_CI_AS) WITH (ROWS_PER_BATCH = 500000)"; + InsertBulk statement = (InsertBulk) assertSqlCanBeParsedAndDeparsed(sql, true, + p -> p.withDialect(Dialect.SQLSERVER)); + assertEquals(10, statement.getColumns().size()); + assertEquals("char(24)", + statement.getColumns().get(9).getColDataType().toString().replace(" ", "")); + assertEquals(List.of("COLLATE", "Chinese_PRC_CI_AS"), + statement.getColumns().get(9).getColumnSpecs()); + assertEquals(InsertBulk.Option.Kind.ROWS_PER_BATCH, + statement.getOptions().get(0).getKind()); + assertEquals("500000", statement.getOptions().get(0).getValue().toString()); + assertEquals(Set.of("tpch.dbo.order_line"), new TablesNamesFinder().getTables(statement)); + assertTrue(statement.getFeatures().modifiesData()); + assertFalse(statement.getFeatures().modifiesSchema()); + assertFalse(statement.getFeatures().returnsResultSet()); + assertEquals(statement.toString(), parse(statement.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"", " WITH (TABLOCK)", + " WITH (CHECK_CONSTRAINTS, FIRE_TRIGGERS, KEEP_NULLS, ALLOW_ENCRYPTED_VALUE_MODIFICATIONS)", + " WITH (ORDER ([id] ASC, [name] DESC), ROWS_PER_BATCH = 100)"}) + void supportsDriverOptionsAndColumnTypes(String options) throws Exception { + String sql = "INSERT BULK [db].[dbo].[items] ([id] decimal(18, 2), [name] nvarchar(max))" + + options; + InsertBulk statement = (InsertBulk) assertSqlCanBeParsedAndDeparsed(sql, true, + p -> p.withDialect(Dialect.SQLSERVER)); + assertEquals(statement.toString(), parse(statement.toString()).toString()); + } + + @Test + void exposesMutableOptionsToVisitorsAndDeparsers() throws Exception { + InsertBulk statement = + parse("INSERT BULK t (id int) WITH (ROWS_PER_BATCH = 100, ORDER (id ASC))"); + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }; + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "context"); + assertEquals(List.of(100L), values); + statement.getColumns().get(0).getColDataType().setDataType("bigint"); + statement.getOptions().get(0).setValue(new LongValue(200)); + statement.getOptions().get(1).getOrderByElements().get(0).setAsc(false); + assertEquals("INSERT BULK t (id bigint) WITH (ROWS_PER_BATCH = 200, ORDER (id DESC))", + statement.toString()); + StringBuilder output = new StringBuilder(); + ExpressionDeParser deparser = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 1); + } + }; + statement.accept(new StatementDeParser(deparser, new SelectDeParser(), output)); + assertEquals(statement.toString().replace("200", "201"), output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + } + + @Test + void gatesDialectAndRetainsOrdinaryInserts() throws Exception { + String sql = "INSERT BULK t (id int)"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.SQLSERVER) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect))); + } + } + assertSqlCanBeParsedAndDeparsed("INSERT INTO bulk (id) VALUES (1)", true, + p -> p.withDialect(Dialect.SQLSERVER)); + assertSqlCanBeParsedAndDeparsed("INSERT INTO [bulk] (id) VALUES (1)", true, + p -> p.withDialect(Dialect.SQLSERVER)); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1;", + p -> p.withDialect(Dialect.SQLSERVER)).size()); + } + + @ParameterizedTest + @ValueSource(strings = {"INSERT BULK t ()", "INSERT BULK t (id)", + "INSERT BULK t (id int) VALUES (1)", + "INSERT BULK t (id int) WITH ()", "INSERT BULK t (id int) WITH (UNKNOWN_OPTION)", + "INSERT BULK t (id int) WITH (TABLOCK = 1)", + "INSERT BULK t (id int) WITH (TABLOCK, TABLOCK)", + "INSERT BULK t (id int) WITH (ROWS_PER_BATCH)", + "INSERT BULK t (id int) WITH (ORDER ())"}) + void rejectsMalformedBulkDeclarations(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void validatesBulkCapability() { + FeatureConfiguration config = CCJSqlParserUtil.newParser("SELECT 1") + .withDialect(Dialect.SQLSERVER).getConfiguration(); + assertTrue(new Validation(config, List.of(SqlServerVersion.values()[0]), + "INSERT BULK t (id int)") + .validate().isEmpty()); + assertFalse(new Validation(config, List.of(new FeaturesAllowed(Feature.insert)), + "INSERT BULK t (id int)").validate().isEmpty()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/insert/InsertTest.java b/src/test/java/net/sf/jsqlparser/statement/insert/InsertTest.java index 18a9019e1e..21e2200c16 100644 --- a/src/test/java/net/sf/jsqlparser/statement/insert/InsertTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/insert/InsertTest.java @@ -677,6 +677,118 @@ public void testInsertOnConflictIssue1551() throws JSQLParserException { true); } + @Test + public void testInsertOnConflictAfterJoinIssue2358() throws JSQLParserException { + String sqlStr = "INSERT INTO conf.supply_info (id, deal_id)\n" + + "SELECT uuid_generate_v4(), rep.deal_id\n" + + "FROM conf.reports rep\n" + + "JOIN conf.orders o ON rep.id = o.report_id\n" + + "ON CONFLICT DO NOTHING"; + Insert insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr, true); + PlainSelect plainSelect = insert.getSelect().getPlainSelect(); + assertEquals(1, plainSelect.getJoins().size()); + assertEquals(1, plainSelect.getJoins().get(0).getOnExpressions().size()); + assertEquals(ConflictActionType.DO_NOTHING, + insert.getConflictAction().getConflictActionType()); + assertNull(insert.getConflictTarget()); + + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO conf.supply_info (id, deal_id)\n" + + "SELECT uuid_generate_v4(), rep.deal_id\n" + + "FROM conf.reports rep\n" + + "JOIN conf.orders o ON rep.id = o.report_id\n" + + "ON CONFLICT (id) DO NOTHING", + true); + + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO distributors (did, dname)\n" + + "SELECT did, dname FROM staging\n" + + "JOIN suppliers s ON staging.did = s.did\n" + + "ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING", + true); + + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO distributors (did, dname)\n" + + "SELECT did, dname FROM staging\n" + + "JOIN suppliers s ON staging.did = s.did\n" + + "ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname", + true); + + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO distributors (did, dname)\n" + + "SELECT did, dname FROM staging\n" + + "JOIN suppliers s ON staging.did = s.did\n" + + "ON CONFLICT (did) WHERE is_active DO NOTHING", + true); + + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO conf.supply_info (id, deal_id)\n" + + "SELECT uuid_generate_v4(), rep.deal_id\n" + + "FROM conf.reports rep\n" + + "JOIN conf.orders o ON rep.id = o.report_id\n" + + "ON CONFLICT DO UPDATE SET deal_id = excluded.deal_id", + true); + + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO conf.supply_info (id, deal_id)\n" + + "SELECT uuid_generate_v4(), rep.deal_id\n" + + "FROM conf.reports rep\n" + + "JOIN conf.orders o ON rep.id = o.report_id\n" + + "ON CONFLICT (id, deal_id) DO NOTHING", + true); + + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO supply_info (id, deal_id)\n" + + "SELECT rep.deal_id, o.total\n" + + "FROM reports rep\n" + + "JOIN orders o ON rep.id = o.report_id\n" + + "JOIN regions r ON o.region = r.id\n" + + "ON CONFLICT DO NOTHING", + true); + + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO conf.supply_info (id, deal_id)\n" + + "SELECT uuid_generate_v4(), rep.deal_id\n" + + "FROM conf.reports rep\n" + + "JOIN conf.orders o ON rep.id = o.report_id\n" + + "ON CONFLICT (id) WHERE (deal_id IN (1, 2)) DO NOTHING", + true); + } + + @Test + public void testInsertSelectWithTrailingOnExpressionsIssue2358() throws JSQLParserException { + String sqlStr = "INSERT INTO supply_info (id, deal_id)\n" + + "SELECT rep.deal_id, o.total\n" + + "FROM reports rep\n" + + "JOIN orders o ON rep.id = o.report_id\n" + + "ON o.total > 100"; + Insert insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr, true); + PlainSelect plainSelect = insert.getSelect().getPlainSelect(); + assertEquals(1, plainSelect.getJoins().size()); + assertEquals(2, plainSelect.getJoins().get(0).getOnExpressions().size()); + assertNull(insert.getConflictAction()); + assertNull(insert.getConflictTarget()); + + insert = (Insert) assertSqlCanBeParsedAndDeparsed( + "INSERT INTO supply_info (id, deal_id)\n" + + "SELECT rep.deal_id, o.total\n" + + "FROM reports rep\n" + + "JOIN orders o ON rep.id = o.report_id\n" + + "ON conflict", + true); + assertEquals(2, + insert.getSelect().getPlainSelect().getJoins().get(0).getOnExpressions().size()); + + insert = (Insert) assertSqlCanBeParsedAndDeparsed( + "INSERT INTO t (a)\n" + + "SELECT x FROM u\n" + + "JOIN v USING (id)\n" + + "ON CONFLICT DO NOTHING", + true); + assertEquals(ConflictActionType.DO_NOTHING, + insert.getConflictAction().getConflictActionType()); + } + @Test public void insertOnConflictObjectsTest() throws JSQLParserException { String sqlStr = "WITH a ( a, b , c ) \n" + "AS (SELECT 1 , 2 , 3 )\n" @@ -966,6 +1078,58 @@ void testInsertOverwrite() throws JSQLParserException { assertFalse(insert.isOverwrite()); } + @Test + // a dynamic partition column without a value must not inherit the value of a + // preceding static partition column (#2500) + void testInsertMixedStaticAndDynamicPartitions() throws JSQLParserException { + String sqlStr = + "INSERT OVERWRITE TABLE t PARTITION (dtime = '2024-04-03', region) SELECT * FROM a"; + Insert insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(2, insert.getPartitions().size()); + assertEquals("dtime", insert.getPartitions().get(0).getColumn().getColumnName()); + assertEquals("'2024-04-03'", insert.getPartitions().get(0).getValue().toString()); + assertEquals("region", insert.getPartitions().get(1).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(1).getValue()); + + sqlStr = + "INSERT OVERWRITE TABLE t PARTITION (region, dtime = '2024-04-03') SELECT * FROM a"; + insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(2, insert.getPartitions().size()); + assertEquals("region", insert.getPartitions().get(0).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(0).getValue()); + assertEquals("dtime", insert.getPartitions().get(1).getColumn().getColumnName()); + assertEquals("'2024-04-03'", insert.getPartitions().get(1).getValue().toString()); + + sqlStr = "INSERT OVERWRITE TABLE t PARTITION" + + " (dtime = '2024-04-03', region, dept) SELECT * FROM a"; + insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(3, insert.getPartitions().size()); + assertEquals("dtime", insert.getPartitions().get(0).getColumn().getColumnName()); + assertEquals("'2024-04-03'", insert.getPartitions().get(0).getValue().toString()); + assertEquals("region", insert.getPartitions().get(1).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(1).getValue()); + assertEquals("dept", insert.getPartitions().get(2).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(2).getValue()); + + sqlStr = "INSERT OVERWRITE TABLE t PARTITION (a, b = '1', c) SELECT * FROM a"; + insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(3, insert.getPartitions().size()); + assertEquals("a", insert.getPartitions().get(0).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(0).getValue()); + assertEquals("b", insert.getPartitions().get(1).getColumn().getColumnName()); + assertEquals("'1'", insert.getPartitions().get(1).getValue().toString()); + assertEquals("c", insert.getPartitions().get(2).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(2).getValue()); + + sqlStr = "INSERT INTO TABLE t PARTITION (dtime = 20240403, region) SELECT * FROM a"; + insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(2, insert.getPartitions().size()); + assertEquals("dtime", insert.getPartitions().get(0).getColumn().getColumnName()); + assertEquals("20240403", insert.getPartitions().get(0).getValue().toString()); + assertNull(insert.getPartitions().get(1).getValue()); + assertFalse(insert.isOverwrite()); + } + @ParameterizedTest @ValueSource(strings = { "INSERT INTO mytable (foo) OVERRIDING SYSTEM VALUE VALUES (1)", diff --git a/src/test/java/net/sf/jsqlparser/statement/insert/PostgreSQLConflictTargetTest.java b/src/test/java/net/sf/jsqlparser/statement/insert/PostgreSQLConflictTargetTest.java new file mode 100644 index 0000000000..189cf81e3a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/insert/PostgreSQLConflictTargetTest.java @@ -0,0 +1,177 @@ +/*- + * #%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.statement.insert; + +import net.sf.jsqlparser.statement.create.table.KeyElement; +import java.util.ArrayList; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.SelectDeParser; + +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class PostgreSQLConflictTargetTest { + @ParameterizedTest + @ValueSource(strings = { + "(id, (lower(email)))", + "(id, lower(email))", + "(pg_catalog.lower(email) COLLATE pg_catalog.\"C\" pg_catalog.text_pattern_ops, id)", + "((lower(email)), id, (abs(region))) WHERE active", + "(email COLLATE \"C\" text_pattern_ops, id)", + "((lower(email)) COLLATE pg_catalog.\"C\" pg_catalog.text_pattern_ops)", + "ON CONSTRAINT users_pkey" + }) + void parsesAndReparsesConflictTargets(String target) throws Exception { + String sql = "INSERT INTO users (id, email) VALUES (1, 'a') ON CONFLICT " + + target + " DO UPDATE SET email = excluded.email RETURNING id"; + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + assertEquals(insert.toString(), CCJSqlParserUtil.parse(insert.toString()).toString()); + } + + @Test + void preservesFunctionKeyParentheses() throws Exception { + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed( + "INSERT INTO users VALUES (1) ON CONFLICT (lower(email), (abs(region))) DO NOTHING", + true); + List keys = insert.getConflictTarget().getIndexElements(); + assertInstanceOf(Function.class, keys.get(0).getExpression()); + assertFalse(keys.get(0).isExpressionParenthesized()); + assertTrue(keys.get(1).isExpressionParenthesized()); + } + + @ParameterizedTest + @ValueSource(strings = {"SYSTEM", "USER"}) + void acceptsOverridingWithoutColumnList(String mode) throws Exception { + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed( + "INSERT INTO users OVERRIDING " + mode + " VALUE VALUES (42, 'a@example.org', 'A')", + true); + assertNull(insert.getTable().getAlias()); + assertEquals(Insert.OverridingMode.valueOf(mode), insert.getOverridingMode()); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "INSERT INTO users overriding VALUES (1)", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "INSERT INTO users AS overriding OVERRIDING " + mode + + " VALUE SELECT * FROM source", + true); + } + + @Test + void exposesOrderedTypedKeys() throws Exception { + Insert insert = (Insert) CCJSqlParserUtil.parse("INSERT INTO users VALUES (1) " + + "ON CONFLICT (id, (lower(email)) COLLATE pg_catalog.\"C\" " + + "pg_catalog.text_pattern_ops, region) WHERE active DO NOTHING"); + InsertConflictTarget target = insert.getConflictTarget(); + List keys = target.getIndexElements(); + assertEquals(3, keys.size()); + assertEquals("id", keys.get(0).getColumnName()); + assertInstanceOf(Function.class, keys.get(1).getExpression()); + assertEquals("pg_catalog.\"C\"", keys.get(1).getCollation()); + assertEquals("pg_catalog.text_pattern_ops", keys.get(1).getOperatorClass()); + assertEquals(Arrays.asList("id", "region"), target.getIndexColumnNames()); + assertNotNull(target.getWhereExpression()); + } + + @Test + void retainsMutableLegacyColumnViewAndReplacementMethods() throws Exception { + InsertConflictTarget target = new InsertConflictTarget("id", null, null, null); + List names = target.getIndexColumnNames(); + names.add("region"); + names.set(0, "tenant"); + names.remove(1); + assertEquals(" (tenant)", target.toString()); + target.setIndexExpression(CCJSqlParserUtil.parseExpression("lower(email)")); + assertTrue(names.isEmpty()); + assertNotNull(target.getIndexExpression()); + target.withIndexColumnName("id").addAllIndexColumnNames(Arrays.asList("region", "tenant")); + assertNull(target.getIndexExpression()); + assertEquals(Arrays.asList("id", "region", "tenant"), names); + names.clear(); + assertTrue(target.getIndexElements().isEmpty()); + } + + @Test + void bulkColumnAdditionsSnapshotTheMutableView() { + InsertConflictTarget target = new InsertConflictTarget("id", null, null, null); + List names = target.getIndexColumnNames(); + assertTrue(target.addAllIndexColumnNames(names)); + assertEquals(List.of("id", "id"), names); + assertTrue(names.addAll(names)); + assertEquals(4, names.size()); + assertTrue(names.addAll(1, names.subList(0, 2))); + assertEquals(6, names.size()); + assertFalse(names.addAll(List.of())); + } + + @Test + void statementVisitorTraversesTargetExpressions() throws Exception { + Insert insert = (Insert) CCJSqlParserUtil.parse("INSERT INTO users VALUES (1) " + + "ON CONFLICT ((lower(email))) WHERE active DO NOTHING"); + List columns = new ArrayList<>(); + ExpressionVisitorAdapter expressions = + new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + columns.add(column.getColumnName()); + return null; + } + }; + insert.accept(new StatementVisitorAdapter( + new SelectVisitorAdapter<>(expressions)), null); + assertEquals(List.of("email", "active"), columns); + } + + @Test + void deparserVisitsConflictExpressionsAndPredicate() throws Exception { + Insert insert = (Insert) CCJSqlParserUtil.parse("INSERT INTO users VALUES (1) " + + "ON CONFLICT ((lower(email))) WHERE active DO NOTHING"); + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + getBuilder().append("x_").append(column.getColumnName()); + return getBuilder(); + } + }; + insert.accept(new StatementDeParser(expressions, + new SelectDeParser(), sql), null); + assertTrue(sql.toString().contains("lower(x_email)"), sql.toString()); + assertTrue(sql.toString().contains("WHERE x_active"), sql.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"VALUES (1)", "SELECT id FROM source"}) + void distinguishesOverridingModes(String source) throws Exception { + for (Insert.OverridingMode mode : Arrays.asList(Insert.OverridingMode.SYSTEM, + Insert.OverridingMode.USER)) { + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed( + "INSERT INTO users (id) OVERRIDING " + mode + " VALUE " + source, true); + assertEquals(mode, insert.getOverridingMode()); + assertTrue(insert.isOverriding()); + insert.setOverriding(false); + assertEquals(Insert.OverridingMode.NONE, insert.getOverridingMode()); + insert.setOverriding(true); + assertEquals(Insert.OverridingMode.SYSTEM, insert.getOverridingMode()); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/lock/PostgreSqlLockTest.java b/src/test/java/net/sf/jsqlparser/statement/lock/PostgreSqlLockTest.java new file mode 100644 index 0000000000..f5897e3ca1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/lock/PostgreSqlLockTest.java @@ -0,0 +1,95 @@ +/*- + * #%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.statement.lock; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlLockTest { + @ParameterizedTest + @ValueSource(strings = {"ACCESS SHARE", "ROW SHARE", "ROW EXCLUSIVE", + "SHARE UPDATE EXCLUSIVE", "SHARE", "SHARE ROW EXCLUSIVE", "EXCLUSIVE", + "ACCESS EXCLUSIVE"}) + void supportsAllPostgreSqlModes(String mode) throws Exception { + LockStatement lock = parse("LOCK TABLE a, public.b IN " + mode + " MODE NOWAIT"); + assertEquals(mode, lock.getLockMode().getValue()); + assertEquals(2, lock.getTargets().size()); + assertTrue(lock.isNoWait()); + roundTrip(lock); + } + + @Test + void preservesScopesNamesAndOmittedKeywords() throws Exception { + LockStatement lock = parse("LOCK ONLY \"a.b\", public.b * NOWAIT"); + assertFalse(lock.isUseTableKeyword()); + assertNull(lock.getLockMode()); + assertEquals(LockStatement.Scope.ONLY, lock.getTargets().get(0).getScope()); + assertEquals(LockStatement.Scope.INCLUDING_DESCENDANTS, + lock.getTargets().get(1).getScope()); + assertEquals("\"a.b\"", lock.getTable().getName()); + assertThat(new TablesNamesFinder().getTables(lock)) + .containsExactlyInAnyOrder("\"a.b\"", "public.b"); + roundTrip(lock); + roundTrip(parse("LOCK TABLE a")); + roundTrip(parse("LOCK a IN SHARE MODE")); + } + + @Test + void legacyTableAccessorsRemainViewsOfTheFirstTarget() throws Exception { + LockStatement lock = parse("LOCK TABLE ONLY a, b *"); + lock.getTargets().get(0).getTable().setName("changed"); + assertEquals("changed", lock.getTable().getName()); + lock.setTable(new Table("replacement")); + assertEquals(2, lock.getTargets().size()); + assertEquals(LockStatement.Scope.ONLY, lock.getTargets().get(0).getScope()); + assertEquals("LOCK TABLE ONLY replacement, b *", lock.toString()); + roundTrip(lock); + LockStatement legacy = new LockStatement(new Table("t"), LockMode.Exclusive, false, 5L); + assertEquals("LOCK TABLE t IN EXCLUSIVE MODE WAIT 5", legacy.toString()); + assertThrows(IllegalStateException.class, () -> legacy.setNoWait(true)); + } + + @Test + void preservesOracleModeAndWait() throws Exception { + String sql = "LOCK TABLE t IN SHARE UPDATE MODE WAIT 5"; + LockStatement lock = (LockStatement) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.ORACLE)); + assertEquals(LockMode.ShareUpdate, lock.getLockMode()); + assertEquals(sql, lock.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"LOCK TABLE", "LOCK TABLE a,", "LOCK TABLE a IN ACCESS MODE", + "LOCK TABLE a IN SHARE UPDATE EXCLUSIVE", "LOCK TABLE ONLY a *", + "LOCK TABLE a NOWAIT WAIT 1"}) + void rejectsIncompleteAndConflictingClauses(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static LockStatement parse(String sql) throws JSQLParserException { + return (LockStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(LockStatement statement) throws Exception { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out), null); + assertEquals(statement.toString(), out.toString()); + assertEquals(statement.toString(), parse(out.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java b/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java index 27c8c4da0c..87d1d14489 100644 --- a/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java @@ -25,6 +25,7 @@ import static net.sf.jsqlparser.test.TestUtils.assertOracleHintExists; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; /** @@ -363,4 +364,36 @@ private static Stream deriveStandardClausesFromOperationsCases() { Arguments.of(Arrays.asList(insert1, insert2, update1, update2, delete1), update1, insert1, true)); } + + @Test + void testMergeRejectsInvalidWhenNotMatchedClausePairings() { + // WHEN MATCHED rows exist in the target: INSERT is not legal + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN INSERT (a) VALUES (1)")); + + // WHEN NOT MATCHED [BY TARGET] rows do not exist in the target: only INSERT is legal + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN UPDATE SET a = 1"); + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN DELETE"); + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY TARGET THEN UPDATE SET a = 1"); + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY TARGET THEN DELETE"); + + // WHEN NOT MATCHED BY SOURCE rows exist in the target: INSERT is not legal + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY SOURCE THEN INSERT (a) VALUES (1)"); + } + + private static void assertInvalidMergePairing(String sql) { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql)); + Throwable cause = exception; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + assertThat(cause).isInstanceOf(IllegalArgumentException.class); + assertThat(cause.getMessage()).contains("WHEN NOT MATCHED"); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/merge/PostgreSQLMergeReturningTest.java b/src/test/java/net/sf/jsqlparser/statement/merge/PostgreSQLMergeReturningTest.java new file mode 100644 index 0000000000..d1937bcf74 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/merge/PostgreSQLMergeReturningTest.java @@ -0,0 +1,78 @@ +/*- + * #%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.statement.merge; + +import java.util.List; +import java.util.ArrayList; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; + +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.ReturningReferenceType; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class PostgreSQLMergeReturningTest { + private static final String MERGE = "MERGE INTO products p USING incoming i ON p.id = i.id " + + "WHEN MATCHED THEN UPDATE SET price = i.price " + + "WHEN NOT MATCHED THEN INSERT (id, price) VALUES (i.id, i.price) "; + + @ParameterizedTest + @ValueSource(strings = {"RETURNING *", "RETURNING merge_action(), p.id, old.price, new.price", + "RETURNING WITH (OLD AS o, NEW AS n) o.price, n.price, n.*"}) + void parsesAndReparsesReturning(String returning) throws Exception { + Merge merge = (Merge) TestUtils.assertSqlCanBeParsedAndDeparsed(MERGE + returning, true); + assertNotNull(merge.getReturningClause()); + assertEquals(merge.toString(), CCJSqlParserUtil.parse(merge.toString()).toString()); + } + + @Test + void normalizesOldAndNewReferences() throws Exception { + Merge merge = (Merge) CCJSqlParserUtil.parse(MERGE + + "RETURNING WITH (OLD AS o, NEW AS n) o.price, n.price"); + Column oldPrice = merge.getReturningClause().get(0).getExpression(Column.class); + assertEquals(ReturningReferenceType.OLD, oldPrice.getReturningReferenceType()); + assertNull(oldPrice.getTable()); + assertEquals(ReturningReferenceType.NEW, + merge.getReturningClause().get(1).getExpression(Column.class) + .getReturningReferenceType()); + } + + @Test + void statementVisitorTraversesReturningExpressions() throws Exception { + Merge merge = (Merge) CCJSqlParserUtil.parse(MERGE + "RETURNING merge_action()"); + List functions = new ArrayList<>(); + ExpressionVisitorAdapter expressions = + new ExpressionVisitorAdapter() { + @Override + public Void visit(Function function, S context) { + functions.add(function.getName()); + return super.visit(function, context); + } + }; + merge.accept(new StatementVisitorAdapter( + new SelectVisitorAdapter<>(expressions)), null); + assertEquals(List.of("merge_action"), functions); + } + + @Test + void discoversTablesInReturningSubquery() throws Exception { + assertTrue(TablesNamesFinder.findTables(MERGE + + "RETURNING (SELECT max(price) FROM history)").contains("history")); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/piped/PipeExpressionDeParserTest.java b/src/test/java/net/sf/jsqlparser/statement/piped/PipeExpressionDeParserTest.java new file mode 100644 index 0000000000..717a303595 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/piped/PipeExpressionDeParserTest.java @@ -0,0 +1,73 @@ +/*- + * #%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.statement.piped; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PipeExpressionDeParserTest { + @Test + void customPrinterVisitsEveryValueOnceAcrossThePipeline() throws Exception { + String sql = "FROM t |> SELECT 1 AS a, 'secret' AS b |> EXTEND a + 2 AS c " + + "|> SET a = 3, b = 'hidden' |> LIMIT 4 OFFSET 5"; + var statement = CCJSqlParserUtil.parse(sql); + String original = statement.toString(); + List visited = new ArrayList<>(); + List strings = new ArrayList<>(); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + visited.add(value.getValue()); + return getBuilder().append(value.getValue() + 100); + } + + @Override + public StringBuilder visit(StringValue value, S context) { + strings.add(value.getValue()); + return getBuilder().append("'masked'"); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(Arrays.asList(1L, 2L, 3L, 4L, 5L), visited); + assertEquals(Arrays.asList("secret", "hidden"), strings); + assertEquals("FROM t\n|> SELECT 101 AS a, 'masked' AS b\n" + + "|> EXTEND a + 102 AS c\n|> SET a = 103, b = 'masked'\n" + + "|> LIMIT 104 OFFSET 105", output.toString()); + assertEquals(original, statement.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"FROM t |> SELECT DISTINCT a + 1 AS b, a * 2 AS c", + "FROM t |> EXTEND COALESCE(a, 1) AS b", + "FROM t |> SET a = 1, b = (SELECT 2)", + "FROM t |> SET (a, b) = (1, 2)", + "FROM t |> LIMIT 1", "FROM t |> LIMIT 1 OFFSET 2"}) + void defaultPrinterPreservesAliasesSubqueriesAndAssignmentBrackets(String sql) + throws Exception { + var statement = CCJSqlParserUtil.parse(sql); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/reindex/ReindexTest.java b/src/test/java/net/sf/jsqlparser/statement/reindex/ReindexTest.java new file mode 100644 index 0000000000..c68ab29664 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/reindex/ReindexTest.java @@ -0,0 +1,166 @@ +/*- + * #%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.statement.reindex; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.reindex.Reindex.Option; +import net.sf.jsqlparser.statement.reindex.Reindex.Target; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ReindexTest { + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + private static Reindex roundTrip(String sql) throws JSQLParserException { + Reindex reindex = assertInstanceOf(Reindex.class, + assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL)); + assertEquals(sql, CCJSqlParserUtil.parse(reindex.toString(), POSTGRESQL).toString()); + return reindex; + } + + @ParameterizedTest + @ValueSource(strings = { + "REINDEX INDEX CONCURRENTLY idx", + "REINDEX TABLE users", + "REINDEX TABLE CONCURRENTLY app.users", + "REINDEX (VERBOSE) INDEX app.idx", + "REINDEX (VERBOSE false, TABLESPACE fast_space, CONCURRENTLY) SCHEMA app", + "REINDEX (CONCURRENTLY ON, VERBOSE 1) TABLE t", + "REINDEX (TABLESPACE 'pg_default') TABLE t", + "REINDEX SCHEMA CONCURRENTLY app", + "REINDEX DATABASE", + "REINDEX DATABASE CONCURRENTLY mydb", + "REINDEX SYSTEM", + "REINDEX SYSTEM mydb"}) + void parsesReindexStatements(String sql) throws JSQLParserException { + roundTrip(sql); + assertInstanceOf(Reindex.class, CCJSqlParserUtil.parse(sql)); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", POSTGRESQL).size()); + } + + @Test + void keepsTargetNameAndOptions() throws JSQLParserException { + Reindex reindex = + roundTrip("REINDEX (VERBOSE, TABLESPACE fast_space) TABLE CONCURRENTLY app.users"); + + assertEquals(Target.TABLE, reindex.getTarget()); + assertTrue(reindex.isConcurrently()); + assertEquals("app.users", reindex.getName().getFullyQualifiedName()); + assertEquals("app.users", reindex.getTable().getFullyQualifiedName()); + assertEquals(2, reindex.getOptions().size()); + assertEquals(Option.Name.VERBOSE, reindex.getOptions().get(0).getName()); + assertNull(reindex.getOptions().get(0).getValue()); + assertEquals(Option.Name.TABLESPACE, reindex.getOptions().get(1).getName()); + assertEquals("fast_space", reindex.getOptions().get(1).getValue()); + } + + @Test + void namesOnlyTheReindexedObject() throws JSQLParserException { + Reindex index = roundTrip("REINDEX INDEX CONCURRENTLY idx"); + assertEquals(Target.INDEX, index.getTarget()); + assertEquals("idx", index.getName().getFullyQualifiedName()); + assertNull(index.getTable()); + assertTrue(new TablesNamesFinder().getTables((Statement) index).isEmpty()); + + Reindex database = roundTrip("REINDEX DATABASE"); + assertEquals(Target.DATABASE, database.getTarget()); + assertNull(database.getName()); + assertFalse(database.isConcurrently()); + assertTrue(database.getOptions().isEmpty()); + + assertThat(TablesNamesFinder.findTables("REINDEX TABLE app.users")) + .containsExactly("app.users"); + } + + @Test + void buildsAReindexStatement() { + Reindex reindex = new Reindex(Target.SCHEMA, new Table("app")).withConcurrently(true) + .addOptions(new Option(Option.Name.VERBOSE, null)); + + assertDeparse(reindex, "REINDEX (VERBOSE) SCHEMA CONCURRENTLY app"); + } + + @Test + void validationAndFeatures() throws JSQLParserException { + assertTrue(new Validation(List.of(PostgresqlVersion.V10), "REINDEX TABLE t").validate() + .isEmpty()); + assertThat(new Validation(List.of(PostgresqlVersion.V10), + "REINDEX TABLE CONCURRENTLY t").validate()).isNotEmpty(); + assertThat(new Validation(List.of(PostgresqlVersion.V10), + "REINDEX (CONCURRENTLY) TABLE t").validate()).isNotEmpty(); + assertTrue(new Validation(List.of(PostgresqlVersion.V12), + "REINDEX TABLE CONCURRENTLY t").validate().isEmpty()); + assertThat(new Validation(List.of(MySqlVersion.V8_0), "REINDEX TABLE t").validate()) + .isNotEmpty(); + + assertThat(CCJSqlParserUtil.parse("REINDEX TABLE t").getFeatures().getCertain()) + .containsExactlyInAnyOrder(StmtFeature.MODIFIES_SCHEMA, StmtFeature.READS_DATA); + } + + @Test + void startsAStatementAmongUnsupportedStatements() throws JSQLParserException { + Statements statements = CCJSqlParserUtil.parseStatements( + "VACUUM t; REINDEX TABLE t; REINDEX t", + parser -> parser.withUnsupportedStatements(true)); + + assertEquals(3, statements.size()); + assertInstanceOf(UnsupportedStatement.class, statements.get(0)); + assertInstanceOf(Reindex.class, statements.get(1)); + assertInstanceOf(UnsupportedStatement.class, statements.get(2)); + } + + @Test + void reindexRemainsAnIdentifier() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT reindex FROM reindex", false, POSTGRESQL); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE reindex (reindex int)", false, POSTGRESQL); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD COLUMN reindex int", false, POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = { + "REINDEX TABLE", + "REINDEX SCHEMA", + "REINDEX INDEX CONCURRENTLY", + "REINDEX INDEX a, b", + "REINDEX VIEW v", + "REINDEX () TABLE t", + "REINDEX (FORCE) TABLE t", + "REINDEX (TABLESPACE) TABLE t", + "REINDEX SCHEMA app.sub"}) + void rejectsInvalidReindexStatements(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, POSTGRESQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/role/MySqlCreateRoleTest.java b/src/test/java/net/sf/jsqlparser/statement/role/MySqlCreateRoleTest.java new file mode 100644 index 0000000000..29f97556cc --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/role/MySqlCreateRoleTest.java @@ -0,0 +1,115 @@ +/*- + * #%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.statement.role; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlCreateRoleTest { + + private static CreateRole parse(String sql, Dialect dialect) throws JSQLParserException { + CreateRole role = assertInstanceOf(CreateRole.class, TestUtils + .assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(dialect))); + assertEquals(sql, CCJSqlParserUtil + .parse(role.toString(), parser -> parser.withDialect(dialect)).toString()); + return role; + } + + private static List names(CreateRole role) { + return role.getRoles().stream().map(UserAccount::toString).collect(Collectors.toList()); + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "MARIADB"}) + void parsesAccountStyleRoleNames(Dialect dialect) throws JSQLParserException { + CreateRole role = parse("CREATE ROLE 'reader', 'writer'@'localhost'", dialect); + + assertEquals(List.of("'reader'", "'writer'@'localhost'"), names(role)); + assertEquals("localhost", role.getRoles().get(1).getHost().getValue()); + assertNull(role.getName()); + assertFalse(role.isIfNotExists()); + assertTrue(role.getOptions().isEmpty()); + } + + @Test + void parsesIfNotExists() throws JSQLParserException { + CreateRole role = parse("CREATE ROLE IF NOT EXISTS app_read, app_write", Dialect.MYSQL); + + assertTrue(role.isIfNotExists()); + assertEquals(List.of("app_read", "app_write"), names(role)); + assertInstanceOf(CreateRole.class, TestUtils + .assertSqlCanBeParsedAndDeparsed("CREATE ROLE IF NOT EXISTS 'reader'", false)); + } + + @Test + void keepsPostgreSqlRoleName() throws JSQLParserException { + // a single unquoted name means the same in both databases and keeps getName() + for (Dialect dialect : List.of(Dialect.MYSQL, Dialect.POSTGRESQL)) { + CreateRole role = parse("CREATE ROLE reader", dialect); + assertEquals("reader", role.getName()); + assertTrue(role.getRoles().isEmpty()); + } + CreateRole role = parse("CREATE ROLE reader WITH LOGIN", Dialect.POSTGRESQL); + assertEquals(RoleOption.Kind.LOGIN, role.getOptions().get(0).getKind()); + } + + @Test + void splitsAnUnquotedNameAtHostOnlyWithMySqlDialects() throws JSQLParserException { + CreateRole mySql = parse("CREATE ROLE r@localhost", Dialect.MYSQL); + assertEquals("r", mySql.getRoles().get(0).getUser().getValue()); + assertEquals("localhost", mySql.getRoles().get(0).getHost().getValue()); + + // without a dialect the lexer's single identifier stays the PostgreSQL role name + CreateRole role = assertInstanceOf(CreateRole.class, + TestUtils.assertSqlCanBeParsedAndDeparsed("CREATE ROLE r@localhost", false)); + assertEquals("r@localhost", role.getName()); + assertEquals(List.of("r", "'%'"), List.of( + parse("CREATE ROLE r@'%'", Dialect.ANSI_SQL).getRoles().get(0).getUser().getValue(), + parse("CREATE ROLE r@'%'", Dialect.ANSI_SQL).getRoles().get(0).getHost() + .toString())); + } + + @Test + void buildsAndDeparses() { + CreateRole role = new CreateRole(); + role.setIfNotExists(true); + role.setRoles(List.of(new UserAccount().withUser(new StringValue("r1")), + new UserAccount().withUser(new StringValue("r2")).withHost(new StringValue("%")))); + + assertDeparse(role, "CREATE ROLE IF NOT EXISTS 'r1', 'r2'@'%'"); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE OR REPLACE ROLE 'r'", "CREATE ROLE 'r' WITH LOGIN", + "CREATE ROLE IF NOT EXISTS", "CREATE ROLE 'r'@'h' IDENTIFIED BY 'x'", + "CREATE ROLE r1, r2 LOGIN"}) + void rejectsMixedSyntax(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/securitylabel/SecurityLabelTest.java b/src/test/java/net/sf/jsqlparser/statement/securitylabel/SecurityLabelTest.java new file mode 100644 index 0000000000..211abadac4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/securitylabel/SecurityLabelTest.java @@ -0,0 +1,218 @@ +/*- + * #%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.statement.securitylabel; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel.ObjectType; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SecurityLabelTest { + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + private static SecurityLabel roundTrip(String sql) throws JSQLParserException { + SecurityLabel label = assertInstanceOf(SecurityLabel.class, + assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL)); + assertEquals(sql, CCJSqlParserUtil.parse(label.toString(), POSTGRESQL).toString()); + return label; + } + + @ParameterizedTest + @ValueSource(strings = { + "SECURITY LABEL FOR selinux ON TABLE t1 IS 'system_u:object_r:sepgsql_table_t:s0'", + "SECURITY LABEL FOR 'dummy' ON TABLE app.t1 IS NULL", + "SECURITY LABEL ON COLUMN app.t1.c IS 'x'", + "SECURITY LABEL ON SEQUENCE app.s IS 'x'", + "SECURITY LABEL ON VIEW v IS 'x'", + "SECURITY LABEL ON MATERIALIZED VIEW app.mv IS 'x'", + "SECURITY LABEL ON FOREIGN TABLE ft IS 'x'", + "SECURITY LABEL ON FUNCTION app.f(int, text) IS 'x'", + "SECURITY LABEL ON FUNCTION f IS 'x'", + "SECURITY LABEL ON PROCEDURE p() IS 'x'", + "SECURITY LABEL ON ROUTINE r(IN a int) IS 'x'", + "SECURITY LABEL ON AGGREGATE agg(*) IS 'x'", + "SECURITY LABEL ON SCHEMA app IS 'x'", + "SECURITY LABEL ON DATABASE mydb IS 'x'", + "SECURITY LABEL ON ROLE app_user IS 'x'", + "SECURITY LABEL ON TABLESPACE fast_space IS 'x'", + "SECURITY LABEL ON DOMAIN app.positive IS 'x'", + "SECURITY LABEL ON TYPE app.mood IS 'x'", + "SECURITY LABEL ON EVENT TRIGGER etg IS 'x'", + "SECURITY LABEL ON LARGE OBJECT 16385 IS 'x'", + "SECURITY LABEL ON LANGUAGE plpgsql IS 'x'", + "SECURITY LABEL ON PROCEDURAL LANGUAGE plpgsql IS 'x'", + "SECURITY LABEL ON PUBLICATION pub IS 'x'", + "SECURITY LABEL ON SUBSCRIPTION sub IS 'x'"}) + void parsesEveryDocumentedObjectKind(String sql) throws JSQLParserException { + roundTrip(sql); + assertInstanceOf(SecurityLabel.class, CCJSqlParserUtil.parse(sql)); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", POSTGRESQL).size()); + } + + @Test + void keepsProviderTargetAndLabel() throws JSQLParserException { + SecurityLabel label = roundTrip( + "SECURITY LABEL FOR selinux ON TABLE app.t1 IS 'system_u:object_r:sepgsql_table_t:s0'"); + assertEquals("selinux", label.getProvider()); + assertEquals(ObjectType.TABLE, label.getObjectType()); + assertEquals("app.t1", label.getObjectName().getFullyQualifiedName()); + assertEquals("system_u:object_r:sepgsql_table_t:s0", label.getLabel().getValue()); + + SecurityLabel column = roundTrip("SECURITY LABEL ON COLUMN app.t1.c IS NULL"); + assertNull(column.getProvider()); + assertEquals(ObjectType.COLUMN, column.getObjectType()); + assertEquals("c", column.getColumn().getColumnName()); + assertEquals("app.t1", column.getColumn().getTable().getFullyQualifiedName()); + assertNull(column.getLabel()); + + SecurityLabel function = roundTrip("SECURITY LABEL ON FUNCTION f(int, text) IS 'x'"); + assertEquals(ObjectType.FUNCTION, function.getObjectType()); + assertEquals("f", function.getRoutine().getName()); + assertEquals(2, function.getRoutine().getArguments().size()); + assertNull(function.getObjectName()); + + SecurityLabel largeObject = roundTrip("SECURITY LABEL ON LARGE OBJECT 16385 IS 'x'"); + assertEquals(ObjectType.LARGE_OBJECT, largeObject.getObjectType()); + assertEquals(16385L, largeObject.getLargeObjectOid()); + + SecurityLabel language = + roundTrip("SECURITY LABEL ON PROCEDURAL LANGUAGE plpgsql IS 'x'"); + assertEquals(ObjectType.LANGUAGE, language.getObjectType()); + assertTrue(language.isProcedural()); + } + + @Test + void tableVisitorsSeeOnlyRelations() throws JSQLParserException { + assertThat(TablesNamesFinder.findTables("SECURITY LABEL ON TABLE app.t1 IS 'x'")) + .containsExactly("app.t1"); + assertThat(TablesNamesFinder.findTables("SECURITY LABEL ON COLUMN app.t1.c IS 'x'")) + .containsExactly("app.t1"); + assertThat(TablesNamesFinder.findTables("SECURITY LABEL ON FOREIGN TABLE ft IS 'x'")) + .containsExactly("ft"); + for (String sql : List.of("SECURITY LABEL ON FUNCTION f(int) IS 'x'", + "SECURITY LABEL ON SCHEMA app IS 'x'", "SECURITY LABEL ON ROLE r IS 'x'", + "SECURITY LABEL ON TYPE app.t IS 'x'")) { + assertTrue(TablesNamesFinder.findTables(sql).isEmpty(), sql); + } + } + + @Test + void settersSelectTheObjectType() { + SecurityLabel label = new SecurityLabel().withProvider("selinux") + .withObjectName(ObjectType.VIEW, new Table("app", "v")) + .withLabel(new StringValue("x")); + assertDeparse(label, "SECURITY LABEL FOR selinux ON VIEW app.v IS 'x'"); + + RoutineReference routine = new RoutineReference(); + routine.setName("f"); + label.setRoutine(ObjectType.PROCEDURE, routine); + assertNull(label.getObjectName()); + assertDeparse(label, "SECURITY LABEL FOR selinux ON PROCEDURE f IS 'x'"); + + label.setColumn(new Column(new Table("t"), "c")); + assertNull(label.getRoutine()); + label.setLabel(null); + assertDeparse(label, "SECURITY LABEL FOR selinux ON COLUMN t.c IS NULL"); + + assertThrows(IllegalArgumentException.class, + () -> label.setObjectName(ObjectType.FUNCTION, new Table("f"))); + assertThrows(IllegalArgumentException.class, + () -> label.setRoutine(ObjectType.TABLE, routine)); + } + + @Test + void labelUsesTheExpressionDeparser() throws JSQLParserException { + SecurityLabel label = (SecurityLabel) CCJSqlParserUtil + .parse("SECURITY LABEL ON TABLE t IS 'before'"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + return getBuilder().append("'after'"); + } + }; + SelectDeParser selects = new SelectDeParser(expressions, output); + expressions.setBuilder(output); + expressions.setSelectVisitor(selects); + label.accept(new StatementDeParser(expressions, selects, output)); + assertEquals("SECURITY LABEL ON TABLE t IS 'after'", output.toString()); + } + + @Test + void validationAndFeatures() throws JSQLParserException { + String sql = "SECURITY LABEL FOR selinux ON TABLE t1 IS 'x'"; + assertTrue(new Validation(List.of(PostgresqlVersion.V10), sql).validate().isEmpty()); + assertThat(new Validation(List.of(MySqlVersion.V8_0), sql).validate()).isNotEmpty(); + assertThat(CCJSqlParserUtil.parse(sql).getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertFalse(CCJSqlParserUtil.parse(sql).getFeatures().may(StmtFeature.READS_DATA)); + } + + @Test + void wordsRemainIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT security, label FROM label", false, POSTGRESQL); + } + + @Test + void startsAStatementAmongUnsupportedStatements() throws JSQLParserException { + Statements statements = CCJSqlParserUtil.parseStatements( + "VACUUM t; SECURITY LABEL ON TABLE t IS 'x'", + parser -> parser.withUnsupportedStatements(true)); + + assertEquals(2, statements.size()); + assertInstanceOf(UnsupportedStatement.class, statements.get(0)); + assertInstanceOf(SecurityLabel.class, statements.get(1)); + } + + @ParameterizedTest + @ValueSource(strings = { + "SECURITY LABEL ON TABLE t", + "SECURITY LABEL ON TABLE t IS 1", + "SECURITY LABEL ON INDEX i IS 'x'", + "SECURITY LABEL ON ROLE app.r IS 'x'", + "SECURITY LABEL ON AGGREGATE agg IS 'x'", + "SECURITY LABEL ON LARGE OBJECT lo IS 'x'", + "SECURITY LABEL FOR p IS 'x'", + "SECURITY TABLE t IS 'x'"}) + void rejectsInvalidSecurityLabels(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, POSTGRESQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/AllColumnsTest.java b/src/test/java/net/sf/jsqlparser/statement/select/AllColumnsTest.java index 7f5aaba4c5..c95b5cdc44 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/AllColumnsTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/AllColumnsTest.java @@ -9,7 +9,17 @@ */ package net.sf.jsqlparser.statement.select; +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; + import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ColumnsTransformer; +import net.sf.jsqlparser.expression.ColumnsTransformer.ColumnsTransformerType; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -31,4 +41,56 @@ void testDuckDBQuerySyntax() throws JSQLParserException { + "FROM orders"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void addExceptColumnToNewWildcard() throws JSQLParserException { + AllColumns columns = new AllColumns(); + columns.addTransformer(new ColumnsTransformer(ColumnsTransformerType.EXCEPT) + .setExceptColumns(new ParenthesedExpressionList<>(new Column("hidden")))); + PlainSelect select = new PlainSelect().addSelectItem(columns).withFromItem(new Table("t")); + + String expected = "SELECT * EXCEPT (hidden) FROM t"; + assertEquals(expected, select.toString()); + assertDeparse(select, expected); + assertSqlCanBeParsedAndDeparsed(expected); + } + + @Test + void setExceptColumnsOnParsedWildcard() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT * FROM t"); + AllColumns columns = select.getSelectItem(0).getExpression(AllColumns.class); + columns.addTransformer(new ColumnsTransformer(ColumnsTransformerType.EXCEPT) + .setExceptColumns(new ParenthesedExpressionList<>(new Column("hidden")))); + + String expected = "SELECT * EXCEPT (hidden) FROM t"; + assertEquals(expected, select.toString()); + assertDeparse(select, expected); + assertSqlCanBeParsedAndDeparsed(expected); + } + + @Test + void addExceptColumnToParsedTableWildcard() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT t.* FROM t"); + AllTableColumns columns = select.getSelectItem(0).getExpression(AllTableColumns.class); + columns.addTransformer(new ColumnsTransformer(ColumnsTransformerType.EXCEPT) + .setExceptColumns(new ParenthesedExpressionList<>(new Column("hidden")))); + + String expected = "SELECT t.* EXCEPT (hidden) FROM t"; + assertEquals(expected, select.toString()); + assertDeparse(select, expected); + assertSqlCanBeParsedAndDeparsed(expected); + } + + @Test + void preserveExplicitExcludeTransformer() throws JSQLParserException { + AllColumns columns = new AllColumns(); + columns.addTransformer(new ColumnsTransformer(ColumnsTransformerType.EXCLUDE) + .setExceptColumns(new ParenthesedExpressionList<>(new Column("hidden")))); + PlainSelect select = new PlainSelect().addSelectItem(columns).withFromItem(new Table("t")); + + String expected = "SELECT * EXCLUDE (hidden) FROM t"; + assertEquals(expected, select.toString()); + assertDeparse(select, expected); + assertSqlCanBeParsedAndDeparsed(expected); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/AsOfJoinTest.java b/src/test/java/net/sf/jsqlparser/statement/select/AsOfJoinTest.java new file mode 100644 index 0000000000..1d327ee0cf --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/AsOfJoinTest.java @@ -0,0 +1,114 @@ +/*- + * #%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.statement.select; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Set; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AsOfJoinTest extends ValidationTestAsserts { + @ParameterizedTest + @ValueSource(strings = {"ASOF JOIN", "ASOF INNER JOIN", "ASOF LEFT JOIN", + "ASOF LEFT OUTER JOIN", "ASOF RIGHT JOIN", "ASOF FULL OUTER JOIN"}) + void preservesAsOfAndOrdinaryJoinQualifiers(String joinType) throws Exception { + String sql = "SELECT * FROM trades t " + joinType + + " prices p ON t.symbol = p.symbol AND t.ts >= p.ts"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + Join join = select.getJoins().get(0); + assertTrue(join.isAsOf()); + assertEquals(joinType.contains("LEFT"), join.isLeft()); + assertEquals(joinType.contains("OUTER"), join.isOuter()); + assertTrue(((PlainSelect) CCJSqlParserUtil.parse(select.toString())).getJoins().get(0) + .isAsOf()); + assertEquals(Set.of("trades", "prices"), TablesNamesFinder.findTables(sql)); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * FROM trades ASOF JOIN prices USING (symbol, ts)", + "SELECT * FROM trades ASOF /* nearest */ LEFT JOIN prices USING (symbol, ts)", + "SELECT * FROM (SELECT * FROM trades) ASOF JOIN prices ON trades.ts >= prices.ts", + "SELECT * FROM trades t ASOF JOIN prices p ON t.ts >= p.ts JOIN symbols s ON s.id = t.id", + "FROM trades t |> ASOF JOIN prices p ON t.ts >= p.ts |> SELECT *"}) + void worksWithoutAliasesAndWithinExistingJoinPaths(String sql) throws Exception { + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + void keepsUsingColumnsStructuredAndAllowsChangingTheJoinKind() throws Exception { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT * FROM trades ASOF LEFT JOIN prices USING (symbol, ts)"); + Join join = select.getJoins().get(0); + assertEquals("symbol", join.getUsingColumns().get(0).getColumnName()); + assertEquals("ts", join.getUsingColumns().get(1).getColumnName()); + join.setAsOf(false); + assertEquals("SELECT * FROM trades LEFT JOIN prices USING (symbol, ts)", select.toString()); + assertTrue(join.withAsOf(true).isAsOf()); + } + + @Test + void visitsPredicatesThroughTheConfiguredDeparser() throws Exception { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT * FROM trades t ASOF LEFT JOIN prices p ON t.ts >= p.ts + 1"); + StringBuilder output = new StringBuilder(); + int[] visits = {0}; + ExpressionDeParser expressions = new ExpressionDeParser(null, output) { + @Override + public StringBuilder visit(LongValue value, S context) { + visits[0]++; + return getBuilder().append(value.getValue() + 100); + } + }; + SelectDeParser deparser = new SelectDeParser(expressions, output); + expressions.setSelectVisitor(deparser); + select.accept((SelectVisitor) deparser, null); + assertEquals("SELECT * FROM trades t ASOF LEFT JOIN prices p ON t.ts >= p.ts + 101", + output.toString()); + assertEquals(1, visits[0]); + assertTrue(select.toString().endsWith("p.ts + 1")); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT asof FROM trades", "SELECT * FROM asof", + "SELECT * FROM trades asof WHERE asof.ts > 0", + "SELECT * FROM trades AS asof JOIN prices p ON asof.ts = p.ts", + "SELECT * FROM trades \"ASOF\" JOIN prices p ON \"ASOF\".ts = p.ts"}) + void keepsAsOfUsableAsAnIdentifier(String sql) throws Exception { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + if (select.getJoins() != null) { + assertFalse(select.getJoins().get(0).isAsOf()); + } + } + + @Test + void validatesTheAsOfFeatureAndPredicateExpressions() throws Exception { + String sql = "SELECT * FROM trades t ASOF JOIN prices p ON t.ts >= p.ts"; + validateNoErrors(sql, 1, FeaturesAllowed.SELECT); + validateNotAllowed(sql, 1, 1, + new FeaturesAllowed().add(FeaturesAllowed.SELECT).remove(Feature.joinAsOf), + Feature.joinAsOf); + validateNotAllowed(sql + " + ?", 1, 1, + new FeaturesAllowed().add(FeaturesAllowed.SELECT).remove(Feature.jdbcParameter), + Feature.jdbcParameter); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java index 81a3217844..33a74b2cc0 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java @@ -10,6 +10,12 @@ package net.sf.jsqlparser.statement.select; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ArrayExpression; +import net.sf.jsqlparser.statement.AssertStatement; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; @@ -110,4 +116,233 @@ void testTimeSeriesFunction() throws JSQLParserException { TableFunction function = select.getFromItem(TableFunction.class); Assertions.assertEquals("TABLE", function.getFunction().getExtraKeyword()); } + + @Test + void testArrayOffsetAccessor() throws JSQLParserException { + String sqlStr = "SELECT arr[OFFSET(0)] FROM t"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + ArrayExpression arrayExpression = + (ArrayExpression) select.getSelectItem(0).getExpression(); + + Assertions.assertEquals("OFFSET(0)", arrayExpression.getIndexExpression().toString()); + } + + @Test + void testArraySafeOffsetAndOrdinalAccessors() throws JSQLParserException { + String sqlStr = "SELECT arr[ORDINAL(1)], arr[SAFE_OFFSET(2)], arr[SAFE_ORDINAL(3)] FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testOffsetClauseStillParses() throws JSQLParserException { + String sqlStr = "SELECT a FROM t LIMIT 10 OFFSET 5"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testUnnestWithOffsetAfterAlias() throws JSQLParserException { + String sqlStr = "SELECT x, pos FROM UNNEST([1, 2, 3]) AS x WITH OFFSET AS pos"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + TableFunction unnest = (TableFunction) select.getFromItem(); + + Assertions.assertTrue(unnest.isWithOffset()); + Assertions.assertEquals("x", unnest.getAlias().getName()); + Assertions.assertEquals("pos", unnest.getOffsetAlias().getName()); + } + + @Test + void testUnnestWithOffsetWithoutOffsetAlias() throws JSQLParserException { + String sqlStr = "SELECT * FROM t, UNNEST(t.arr) AS c WITH OFFSET"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testUnnestWithOrdinalityAliasStillBindsToTableFunction() throws JSQLParserException { + String sqlStr = "SELECT * FROM UNNEST(ARRAY[1, 2, 3]) WITH ORDINALITY AS t (a, b)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + TableFunction unnest = (TableFunction) select.getFromItem(); + + Assertions.assertFalse(unnest.isWithOffset()); + Assertions.assertEquals("t", unnest.getAlias().getName()); + } + + @Test + void testAggregateFunctionIgnoreNullsBeforeOrderBy() throws JSQLParserException { + String sqlStr = "SELECT ARRAY_AGG(x IGNORE NULLS ORDER BY x) FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testAggregateFunctionIgnoreNullsOrderByLimit() throws JSQLParserException { + String sqlStr = "SELECT ARRAY_AGG(DISTINCT x IGNORE NULLS ORDER BY x DESC LIMIT 5) FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testAggregateFunctionOrderByWithoutNullHandling() throws JSQLParserException { + String sqlStr = "SELECT ARRAY_AGG(x ORDER BY x) FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCreateTablePartitionByExpression() throws JSQLParserException { + String sqlStr = "CREATE TABLE ds.t (id INT64, ts TIMESTAMP) PARTITION BY DATE(ts)"; + CreateTable createTable = + (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(TablePartitioning.Type.EXPRESSION, + createTable.getPartitioning().getType()); + Assertions.assertEquals("DATE(ts)", + createTable.getPartitioning().getExpression().toString()); + } + + @Test + void testCreateTablePartitionByColumnWithOptions() throws JSQLParserException { + String sqlStr = + "CREATE TABLE ds.t (id INT64, d DATE) PARTITION BY d OPTIONS (description = 'x')"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCreateTableAsSelectPartitionByAndClusterBy() throws JSQLParserException { + String sqlStr = "CREATE TABLE ds.t PARTITION BY d CLUSTER BY id " + + "AS SELECT 1 AS id, CURRENT_DATE() AS d"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCreateTablePartitionByHashStillParses() throws JSQLParserException { + String sqlStr = "CREATE TABLE t (id INT) PARTITION BY HASH (id)"; + CreateTable createTable = + (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(TablePartitioning.Type.HASH, + createTable.getPartitioning().getType()); + } + + @Test + void testAssertStatementWithDescription() throws JSQLParserException { + String sqlStr = "ASSERT (SELECT COUNT(*) FROM t) > 0 AS 'table is empty'"; + AssertStatement assertStatement = + (AssertStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("'table is empty'", assertStatement.getDescription().toString()); + } + + @Test + void testAssertStatementWithoutDescription() throws JSQLParserException { + String sqlStr = "ASSERT EXISTS(SELECT 1 FROM t)"; + AssertStatement assertStatement = + (AssertStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(assertStatement.getDescription()); + } + + @Test + void testAssertRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT assert FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testExportData() throws JSQLParserException { + String sqlStr = "EXPORT DATA OPTIONS (uri = 'gs://bucket/*.csv', format = 'CSV', " + + "overwrite = true) AS SELECT * FROM t"; + ExportDataStatement exportData = + (ExportDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(3, exportData.getOptions().size()); + Assertions.assertNull(exportData.getConnectionName()); + } + + @Test + void testExportDataWithConnection() throws JSQLParserException { + String sqlStr = "EXPORT DATA WITH CONNECTION myproject.us.myconnection " + + "OPTIONS (uri = 'gs://bucket/*.csv') AS SELECT a FROM t"; + ExportDataStatement exportData = + (ExportDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("myproject.us.myconnection", exportData.getConnectionName()); + } + + @Test + void testExportDataTablesNamesFinder() throws JSQLParserException { + String sqlStr = "EXPORT DATA OPTIONS (uri = 'gs://bucket/*.csv') AS SELECT * FROM ds.t"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testLoadDataIntoFromFiles() throws JSQLParserException { + String sqlStr = "LOAD DATA INTO mydataset.mytable " + + "FROM FILES (format = 'AVRO', uris = ['gs://bucket/*.avro'])"; + LoadDataStatement loadData = + (LoadDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertFalse(loadData.isOverwrite()); + Assertions.assertEquals("mydataset.mytable", loadData.getTable().getFullyQualifiedName()); + Assertions.assertEquals(2, loadData.getFileOptions().size()); + } + + @Test + void testLoadDataOverwriteWithColumnDefinitions() throws JSQLParserException { + String sqlStr = "LOAD DATA OVERWRITE ds.t (id INT64, name STRING) " + + "FROM FILES (format = 'CSV', uris = ['gs://b/f.csv'])"; + LoadDataStatement loadData = + (LoadDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(loadData.isOverwrite()); + Assertions.assertEquals(2, loadData.getColumnDefinitions().size()); + } + + @Test + void testLoadDataWithOptionsAndConnection() throws JSQLParserException { + String sqlStr = "LOAD DATA INTO ds.t OPTIONS (description = 'x') " + + "FROM FILES (format = 'PARQUET') WITH PARTITION COLUMNS " + + "WITH CONNECTION myproject.us.conn"; + LoadDataStatement loadData = + (LoadDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("myproject.us.conn", loadData.getConnectionName()); + Assertions.assertTrue(loadData.isWithPartitionColumns()); + } + + @Test + void testLoadDataTablesNamesFinder() throws JSQLParserException { + String sqlStr = "LOAD DATA INTO ds.t FROM FILES (format = 'CSV')"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testCreateSnapshotTableClone() throws JSQLParserException { + String sqlStr = "CREATE SNAPSHOT TABLE ds.snap CLONE ds.t"; + CreateTable createTable = + (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("ds.t", createTable.getCloneTable().getFullyQualifiedName()); + } + + @Test + void testCreateSnapshotTableCloneForSystemTimeAsOf() throws JSQLParserException { + String sqlStr = "CREATE SNAPSHOT TABLE IF NOT EXISTS ds.snap CLONE ds.t " + + "FOR SYSTEM_TIME AS OF CURRENT_TIMESTAMP()"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCreateTableCloneWithOptions() throws JSQLParserException { + String sqlStr = "CREATE TABLE ds.copy CLONE ds.t OPTIONS (description = 'x')"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCloneRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT clone FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java index 90f69ffd43..76b92ad67e 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java @@ -11,11 +11,25 @@ import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.ColumnsExpression; +import net.sf.jsqlparser.expression.ColumnsTransformer; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LambdaExpression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class ClickHouseTest { @@ -147,6 +161,105 @@ public void testMultipleSettingsClauseIssue2362() throws JSQLParserException { Assertions.assertEquals(2, select.getSettings().size()); } + @Test + public void testOrderByWithFillIssue2467() throws JSQLParserException { + String sql = + "SELECT d, v FROM t ORDER BY d WITH FILL FROM toDate('2024-01-01') TO toDate('2024-02-01') STEP INTERVAL 1 DAY INTERPOLATE (v AS v + 1)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + + WithFill withFill = select.getOrderByElements().get(0).getWithFill(); + Assertions.assertNotNull(withFill); + Assertions.assertEquals("toDate('2024-01-01')", withFill.getFrom().toString()); + Assertions.assertEquals("toDate('2024-02-01')", withFill.getTo().toString()); + Assertions.assertEquals("INTERVAL 1 DAY", withFill.getStep().toString()); + Assertions.assertNull(withFill.getStaleness()); + + Assertions.assertEquals(1, select.getInterpolate().size()); + Assertions.assertEquals("v", select.getInterpolate().get(0).getColumn().getColumnName()); + Assertions.assertEquals("v + 1", select.getInterpolate().get(0).getExpression().toString()); + } + + @Test + public void testOrderByWithFillOnly() throws JSQLParserException { + String sql = "SELECT key, value FROM t ORDER BY key WITH FILL"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertNotNull(select.getOrderByElements().get(0).getWithFill()); + Assertions.assertNull(select.getOrderByElements().get(0).getWithFill().getFrom()); + } + + @Test + public void testOrderByWithFillFromToStep() throws JSQLParserException { + String sql = "SELECT n FROM t ORDER BY n WITH FILL FROM 0 TO 5.51 STEP 0.5"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + WithFill withFill = select.getOrderByElements().get(0).getWithFill(); + Assertions.assertEquals("0", withFill.getFrom().toString()); + Assertions.assertEquals("5.51", withFill.getTo().toString()); + Assertions.assertEquals("0.5", withFill.getStep().toString()); + } + + @Test + public void testOrderByWithFillStepIntervalAndStaleness() throws JSQLParserException { + String sql = + "SELECT d1, d2 FROM t ORDER BY d1 WITH FILL STEP INTERVAL 1 DAY STALENESS 3, d2 WITH FILL"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(2, select.getOrderByElements().size()); + WithFill first = select.getOrderByElements().get(0).getWithFill(); + Assertions.assertEquals("INTERVAL 1 DAY", first.getStep().toString()); + Assertions.assertEquals("3", first.getStaleness().toString()); + Assertions.assertNotNull(select.getOrderByElements().get(1).getWithFill()); + } + + @Test + public void testOrderByWithFillDescNullsLast() throws JSQLParserException { + String sql = "SELECT d FROM t ORDER BY d DESC NULLS LAST WITH FILL TO 10"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OrderByElement element = select.getOrderByElements().get(0); + Assertions.assertFalse(element.isAsc()); + Assertions.assertEquals(OrderByElement.NullOrdering.NULLS_LAST, element.getNullOrdering()); + Assertions.assertEquals("10", element.getWithFill().getTo().toString()); + } + + @Test + public void testOrderByWithFillInterpolateVariants() throws JSQLParserException { + // multiple items, with and without AS + String sql = "SELECT a, b, c FROM t ORDER BY a WITH FILL INTERPOLATE (b, c AS c + 1)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(2, select.getInterpolate().size()); + Assertions.assertNull(select.getInterpolate().get(0).getExpression()); + Assertions.assertEquals("c + 1", select.getInterpolate().get(1).getExpression().toString()); + + // bare INTERPOLATE fills all allowed columns + sql = "SELECT a, b FROM t ORDER BY a WITH FILL INTERPOLATE"; + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertNotNull(select.getInterpolate()); + Assertions.assertTrue(select.getInterpolate().isEmpty()); + } + + @Test + public void testOrderByWithFillUnionInterpolate() throws JSQLParserException { + String sql = + "SELECT a FROM t1 UNION SELECT a FROM t2 ORDER BY a WITH FILL INTERPOLATE (a AS a + 1) LIMIT 10"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(sql, true); + SetOperationList setOperationList = (SetOperationList) select.getSelectBody(); + Assertions.assertNotNull(setOperationList.getOrderByElements().get(0).getWithFill()); + Assertions.assertEquals(1, setOperationList.getInterpolate().size()); + } + + @Test + public void testOrderByWithFillSubquery() throws JSQLParserException { + String sql = "SELECT * FROM (SELECT d, v FROM t ORDER BY d WITH FILL FROM 0 TO 9) AS sub"; + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testFillStepInterpolateStalenessAsColumnNames() throws JSQLParserException { + // the new keywords stay non-reserved and must remain usable as plain column names + assertSqlCanBeParsedAndDeparsed("SELECT fill, step, staleness FROM t ORDER BY interpolate", + true); + assertSqlCanBeParsedAndDeparsed("SELECT interpolate FROM t WHERE fill = 1 AND step = 2", + true); + } + @Test public void testCastToNestedParametricTypeIssue2441() throws JSQLParserException { // ClickHouse allows parametric (constructor-style) data types as a CAST target, @@ -158,4 +271,638 @@ public void testCastToNestedParametricTypeIssue2441() throws JSQLParserException sql = "SELECT CAST(x AS LowCardinality(Decimal(10, 2))) FROM cast_demo"; assertSqlCanBeParsedAndDeparsed(sql, true); } + + @Test + public void testColumnsApplyLambdaIssue2631() throws JSQLParserException { + // ClickHouse applies a function or lambda expression to all columns matching + // the regular expression: https://github.com/JSQLParser/JSqlParser/issues/2631 + String sql = "SELECT COLUMNS('^metric_') APPLY(x -> round(x, 2)) FROM metrics"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + ColumnsExpression columnsExpression = Assertions.assertInstanceOf(ColumnsExpression.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertEquals(1, columnsExpression.getTransformers().size()); + ColumnsTransformer transformer = columnsExpression.getTransformers().get(0); + Assertions.assertEquals(ColumnsTransformer.ColumnsTransformerType.APPLY, + transformer.getType()); + Assertions.assertInstanceOf(LambdaExpression.class, transformer.getApplyExpression()); + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testColumnsApplyFunctionName() throws JSQLParserException { + // chained APPLY modifiers, taken from the ClickHouse SELECT documentation + String sql = "SELECT COLUMNS('[jk]') APPLY(toString) APPLY(length) APPLY(max)" + + " FROM columns_transformers"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + ColumnsExpression columnsExpression = Assertions.assertInstanceOf(ColumnsExpression.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertEquals(3, columnsExpression.getTransformers().size()); + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testColumnsExcept() throws JSQLParserException { + String sql = "SELECT COLUMNS('^metric_') EXCEPT (metric_disk) FROM metrics"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + ColumnsExpression columnsExpression = Assertions.assertInstanceOf(ColumnsExpression.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertEquals(ColumnsTransformer.ColumnsTransformerType.EXCEPT, + columnsExpression.getTransformers().get(0).getType()); + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testColumnsReplace() throws JSQLParserException { + String sql = + "SELECT COLUMNS('^metric_') REPLACE(metric_cpu * 100 AS metric_cpu) FROM metrics"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + ColumnsExpression columnsExpression = Assertions.assertInstanceOf(ColumnsExpression.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertEquals(ColumnsTransformer.ColumnsTransformerType.REPLACE, + columnsExpression.getTransformers().get(0).getType()); + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testColumnsCombinedTransformers() throws JSQLParserException { + // ClickHouse parses its transformers in a loop, so they combine in any order + String sql = + "SELECT COLUMNS('m') APPLY(x -> round(x, 2)) EXCEPT (metric_disk) FROM metrics"; + assertSqlCanBeParsedAndDeparsed(sql, true); + + sql = "SELECT COLUMNS('m') EXCEPT (metric_disk) APPLY(x -> round(x, 2)) FROM metrics"; + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testAllColumnsApplyIssue2636() throws JSQLParserException { + // ClickHouse accepts the same transformers after '*': + // https://github.com/JSQLParser/JSqlParser/issues/2636 + String sql = "SELECT * APPLY(sum) FROM t"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + SelectItem selectItem = plainSelect.getSelectItems().get(0); + + Assertions.assertNull(selectItem.getAlias(), + "APPLY must not fall back to an alias"); + Assertions.assertInstanceOf(AllColumns.class, selectItem.getExpression()); + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testAllColumnsChainedTransformersIssue2636() throws JSQLParserException { + // ClickHouse parses the transformers in a loop, so they may repeat after '*' + String sql = "SELECT * APPLY(sum) EXCEPT (a) FROM t"; + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testAllColumnsExceptStrictIssue2636() throws JSQLParserException { + // STRICT sits between the EXCEPT keyword and its column list + // (ClickHouse ExpressionElementParsers.cpp, ParserColumnsTransformers) + String sql = "SELECT * EXCEPT STRICT (a, b) FROM t"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + SelectItem selectItem = plainSelect.getSelectItems().get(0); + AllColumns allColumns = + Assertions.assertInstanceOf(AllColumns.class, selectItem.getExpression()); + ColumnsTransformer transformer = allColumns.getTransformers().get(0); + Assertions.assertEquals(ColumnsTransformer.ColumnsTransformerType.EXCEPT, + transformer.getType()); + Assertions.assertTrue(transformer.isStrict()); + Assertions.assertInstanceOf(ParenthesedExpressionList.class, + transformer.getExceptColumns()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + // the qualified '*' shares the same transformer production + sql = "SELECT t.* EXCEPT STRICT (a) FROM t"; + select = (Select) CCJSqlParserUtil.parse(sql); + plainSelect = (PlainSelect) select.getSelectBody(); + AllTableColumns allTableColumns = Assertions.assertInstanceOf(AllTableColumns.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertTrue(allTableColumns.getTransformers().get(0).isStrict()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + // the flag belongs to one transformer of the chain only + sql = "SELECT * EXCEPT STRICT (a) REPLACE(x AS a) FROM t"; + select = (Select) CCJSqlParserUtil.parse(sql); + plainSelect = (PlainSelect) select.getSelectBody(); + allColumns = Assertions.assertInstanceOf(AllColumns.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertEquals(2, allColumns.getTransformers().size()); + Assertions.assertTrue(allColumns.getTransformers().get(0).isStrict()); + Assertions.assertFalse(allColumns.getTransformers().get(1).isStrict()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + // the transformer survives as the second leg of a set operation + sql = "SELECT 1 UNION SELECT * EXCEPT STRICT (a) FROM t"; + SetOperationList union = (SetOperationList) ((Select) CCJSqlParserUtil.parse(sql)) + .getSelectBody(); + PlainSelect rightLeg = (PlainSelect) union.getSelects().get(1); + AllColumns legColumns = Assertions.assertInstanceOf(AllColumns.class, + rightLeg.getSelectItems().get(0).getExpression()); + Assertions.assertTrue(legColumns.getTransformers().get(0).isStrict()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + // the COLUMNS(...) matcher accepts the same STRICT form + sql = "SELECT COLUMNS('m') EXCEPT STRICT (a) FROM t"; + select = (Select) CCJSqlParserUtil.parse(sql); + plainSelect = (PlainSelect) select.getSelectBody(); + ColumnsExpression columnsExpression = Assertions.assertInstanceOf(ColumnsExpression.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertTrue(columnsExpression.getTransformers().get(0).isStrict()); + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testAllColumnsReplaceStrictIssue2636() throws JSQLParserException { + String sql = "SELECT * REPLACE STRICT(1 AS a, 2 AS b) FROM t"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + AllColumns allColumns = Assertions.assertInstanceOf(AllColumns.class, + plainSelect.getSelectItems().get(0).getExpression()); + ColumnsTransformer transformer = allColumns.getTransformers().get(0); + Assertions.assertEquals(ColumnsTransformer.ColumnsTransformerType.REPLACE, + transformer.getType()); + Assertions.assertTrue(transformer.isStrict()); + Assertions.assertEquals(2, transformer.getReplaceItems().size()); + Assertions.assertEquals("a", transformer.getReplaceItems().get(0).getAlias().getName()); + Assertions.assertEquals(sql, select.toString()); + StatementDeParser statementDeParser = new StatementDeParser(new StringBuilder()); + select.accept(statementDeParser, null); + Assertions.assertEquals(sql, statementDeParser.getBuilder().toString()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + sql = "SELECT COLUMNS('m') REPLACE STRICT(x + 1 AS x) FROM t"; + select = (Select) CCJSqlParserUtil.parse(sql); + plainSelect = (PlainSelect) select.getSelectBody(); + Assertions.assertInstanceOf(ColumnsExpression.class, + plainSelect.getSelectItems().get(0).getExpression()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + // the qualified '*' copies the transformers through its own constructor + sql = "SELECT t.* REPLACE STRICT(x AS x) FROM t"; + select = (Select) CCJSqlParserUtil.parse(sql); + plainSelect = (PlainSelect) select.getSelectBody(); + AllTableColumns qualifiedStar = Assertions.assertInstanceOf(AllTableColumns.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertTrue(qualifiedStar.getTransformers().get(0).isStrict()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + // each transformer of the chain carries its own flag + sql = "SELECT * REPLACE STRICT(x AS x) REPLACE(y AS y) FROM t"; + select = (Select) CCJSqlParserUtil.parse(sql); + plainSelect = (PlainSelect) select.getSelectBody(); + allColumns = Assertions.assertInstanceOf(AllColumns.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertEquals(2, allColumns.getTransformers().size()); + Assertions.assertTrue(allColumns.getTransformers().get(0).isStrict()); + Assertions.assertFalse(allColumns.getTransformers().get(1).isStrict()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + sql = "SELECT * EXCEPT (a) REPLACE STRICT(x AS a) APPLY(sum) FROM t"; + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testAllColumnsExceptPatternIssue2636() throws JSQLParserException { + // ClickHouse accepts a re2 pattern string instead of the column list and always + // prints it without brackets (ASTColumnsTransformers.cpp) + String sql = "SELECT * EXCEPT '^tmp_' FROM t"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + AllColumns allColumns = Assertions.assertInstanceOf(AllColumns.class, + plainSelect.getSelectItems().get(0).getExpression()); + ColumnsTransformer transformer = allColumns.getTransformers().get(0); + Assertions.assertEquals(ColumnsTransformer.ColumnsTransformerType.EXCEPT, + transformer.getType()); + Assertions.assertNull(transformer.getExceptColumns()); + Assertions.assertEquals("^tmp_", transformer.getExceptPattern().getValue()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + sql = "SELECT t.* EXCEPT '^tmp_' FROM t"; + assertSqlCanBeParsedAndDeparsed(sql, true); + + sql = "SELECT COLUMNS('m') EXCEPT '^tmp_' FROM t"; + select = (Select) CCJSqlParserUtil.parse(sql); + plainSelect = (PlainSelect) select.getSelectBody(); + ColumnsExpression columnsExpression = Assertions.assertInstanceOf(ColumnsExpression.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertEquals("^tmp_", + columnsExpression.getTransformers().get(0).getExceptPattern().getValue()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + // ClickHouse parses STRICT before a pattern as well (it drops the flag only + // during analysis), so the parser keeps it on the AST + sql = "SELECT * EXCEPT STRICT '^tmp_' FROM t"; + select = (Select) CCJSqlParserUtil.parse(sql); + plainSelect = (PlainSelect) select.getSelectBody(); + allColumns = Assertions.assertInstanceOf(AllColumns.class, + plainSelect.getSelectItems().get(0).getExpression()); + Assertions.assertTrue(allColumns.getTransformers().get(0).isStrict()); + Assertions.assertEquals("^tmp_", + allColumns.getTransformers().get(0).getExceptPattern().getValue()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + // the same combination reaches the COLUMNS(...) path through its own gate + sql = "SELECT COLUMNS('m') EXCEPT STRICT '^tmp2_' FROM t"; + assertSqlCanBeParsedAndDeparsed(sql, true); + + // the pattern participates in the expression traversal of the COLUMNS side + // (getAllExpressions -> ExpressionVisitorAdapter), like the other payloads + List visitedValues = new ArrayList<>(); + ExpressionVisitorAdapter adapter = new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue stringValue, Object context) { + visitedValues.add(stringValue.getValue()); + return null; + } + }; + select = (Select) CCJSqlParserUtil.parse("SELECT COLUMNS('m') EXCEPT '^tmp_' FROM t"); + plainSelect = (PlainSelect) select.getSelectBody(); + plainSelect.getSelectItems().get(0).getExpression().accept(adapter, null); + Assertions.assertTrue(visitedValues.contains("^tmp_"), + "the EXCEPT pattern must be reachable for expression visitors"); + + // programmatic construction round-trips through the same output paths + AllColumns built = new AllColumns(); + built.addTransformer( + new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCEPT) + .setStrict(true).setExceptPattern(new StringValue("'inner_.*'"))); + Assertions.assertEquals("* EXCEPT STRICT 'inner_.*'", built.toString()); + } + + @Test + public void testStrictKeywordStaysIdentifierAndAliasIssue2636() throws JSQLParserException { + // STRICT is a non-reserved keyword, so plain identifier uses must keep parsing + assertSqlCanBeParsedAndDeparsed("SELECT strict FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT * AS strict FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT t.strict FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT * EXCEPT (strict) FROM t", true); + + // a trailing STRICT is not part of a transformer: it stays the alias of '*' + String sql = "SELECT * REPLACE(x AS y) STRICT FROM t"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + SelectItem selectItem = plainSelect.getSelectItems().get(0); + Assertions.assertEquals("STRICT", selectItem.getAlias().getName()); + Assertions.assertInstanceOf(AllColumns.class, selectItem.getExpression()); + assertSqlCanBeParsedAndDeparsed(sql, true); + + // a column named strict and a trailing STRICT alias coexist on one item + assertSqlCanBeParsedAndDeparsed("SELECT * EXCEPT (strict) STRICT FROM t", true); + + // the parenthesized set operation after '*' is unchanged + assertSqlCanBeParsedAndDeparsed("SELECT * EXCEPT (SELECT b FROM u)", true); + } + + @Test + public void testStrictTransformerFormsRejectedIssue2636() { + assertThrowsParseException("SELECT * EXCEPT STRICT FROM t"); + assertThrowsParseException("SELECT * REPLACE STRICT FROM t"); + assertThrowsParseException("SELECT * EXCEPT STRICT a FROM t"); + assertThrowsParseException("SELECT * EXCEPT ('^tmp_') FROM t"); + assertThrowsParseException("SELECT * EXCEPT 5 FROM t"); + // neither APPLY nor EXCLUDE has a STRICT variant + assertThrowsParseException("SELECT * APPLY STRICT (x) FROM t"); + assertThrowsParseException("SELECT * EXCLUDE STRICT (a) FROM t"); + assertThrowsParseException("SELECT COLUMNS('m') EXCEPT STRICT a FROM t"); + assertThrowsParseException("SELECT a EXCEPT STRICT FROM t"); + // the pattern and the parenthesized column list are mutually exclusive payloads + assertThrowsParseException("SELECT * EXCEPT STRICT '^x' (a) FROM t"); + assertThrowsParseException("SELECT * REPLACE STRICT () FROM t"); + } + + private static void assertThrowsParseException(String sql) { + Assertions.assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql), + "Expected a parse failure for: " + sql); + } + + @Test + public void testColumnsApplyMultiParamLambda() throws JSQLParserException { + String sql = "SELECT COLUMNS('m') APPLY((k, v) -> v > 5) FROM metrics"; + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testBareColumnsExpressionStaysFunction() throws JSQLParserException { + // without a transformer, COLUMNS(...) keeps parsing as a regular function + String sql = "SELECT COLUMNS('^metric_') FROM metrics"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + Assertions.assertInstanceOf(Function.class, + plainSelect.getSelectItems().get(0).getExpression()); + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testColumnsItemFollowedBySetOperation() throws JSQLParserException { + // a follower keyword without its parenthesis never starts a transformer, so + // EXCEPT stays a set operation (a bare APPLY or REPLACE stays an alias) + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') EXCEPT SELECT b FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') EXCEPT ALL SELECT b FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT a, COLUMNS('m') EXCEPT SELECT b FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') EXCEPT (SELECT b FROM t)", true); + // parenthesized set operation operands that do not start a column list + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') EXCEPT ((SELECT b FROM t))", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') EXCEPT (VALUES (1, 2))", true); + } + + @Test + public void testBareTransformerKeywordRemainsAlias() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') APPLY FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') REPLACE FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') APPLY", true); + } + + @Test + public void testAliasColumnsBodyRemainsAlias() throws JSQLParserException { + // T(name[type], ...) is the alias-columns reading of the regular alias path, + // not a transformer body (APPLY takes a function name or a lambda in ClickHouse) + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') APPLY(a, b) FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') APPLY(a, b)", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') APPLY(a INT) FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') APPLY(`a`, `b`) FROM t", true); + assertSqlCanBeParsedAndDeparsed( + "SELECT COLUMNS('m') REPLACE(a INT, b VARCHAR(10)) FROM t", true); + } + + @Test + public void testReplaceWithBareIdentifierAsAlias() throws JSQLParserException { + // the plain ClickHouse rename form: REPLACE(column AS alias) + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') REPLACE(b AS c) FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') REPLACE(b AS c, d AS e) FROM t", true); + // keyword-predicate lambda bodies stay on the transformer reading + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') APPLY(x IS NULL) FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT COLUMNS('m') APPLY(x AND y) FROM t", true); + } + + @Test + public void testTypedAliasColumnWithCompoundTimeZoneType() throws JSQLParserException { + // compound timezone types are lexed as a single DT_ZONE token but are still + // alias-columns types, so the input belongs to the alias path + assertSqlCanBeParsedAndDeparsed( + "SELECT COLUMNS('m') APPLY(a TIMESTAMP WITH TIME ZONE) FROM t", true); + assertSqlCanBeParsedAndDeparsed( + "SELECT COLUMNS('m') APPLY(a TIMESTAMP(3) WITH TIME ZONE) FROM t", true); + assertSqlCanBeParsedAndDeparsed( + "SELECT COLUMNS('m') APPLY(a TIMESTAMP WITHOUT TIME ZONE) FROM t", true); + } + + @ParameterizedTest + @ValueSource(strings = {"[x, x + 1]", "x IN (1, 2)", + "CASE x WHEN 1 THEN 0 ELSE 1 END"}) + public void testColumnsApplyExpressionBodies(String body) throws JSQLParserException { + for (String prefix : new String[] {"", " APPLY(sum)"}) { + String sql = "SELECT COLUMNS('m')" + prefix + " APPLY(x -> " + body + ") FROM t"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + ColumnsExpression expression = Assertions.assertInstanceOf(ColumnsExpression.class, + select.getSelectItem(0).getExpression()); + ColumnsTransformer transformer = expression.getTransformers() + .get(expression.getTransformers().size() - 1); + LambdaExpression lambda = Assertions.assertInstanceOf(LambdaExpression.class, + transformer.getApplyExpression()); + Assertions.assertEquals(body, lambda.getExpression().toString()); + } + } + + @Test + public void testColumnsReplaceCaseExpression() throws JSQLParserException { + String sql = "SELECT COLUMNS('m') REPLACE(CASE m WHEN 1 THEN 0 ELSE 1 END AS m) FROM t"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + ColumnsExpression expression = Assertions.assertInstanceOf(ColumnsExpression.class, + select.getSelectItem(0).getExpression()); + SelectItem replacement = expression.getTransformers().get(0).getReplaceItems().get(0); + Assertions.assertEquals("CASE m WHEN 1 THEN 0 ELSE 1 END", + replacement.getExpression().toString()); + Assertions.assertEquals("m", replacement.getAlias().getName()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * FROM (SELECT COLUMNS('m') APPLY(sum) FROM t) q", + "WITH q AS (SELECT COLUMNS('m') APPLY(x -> x + 1) FROM t) SELECT * FROM q", + "SELECT (SELECT COLUMNS('m') APPLY(sum) FROM t) AS result", + "SELECT COLUMNS('m') APPLY(sum) EXCEPT (SELECT b FROM t)", + "SELECT 1 UNION SELECT COLUMNS('m') APPLY(sum) FROM t", + "INSERT INTO dst SELECT COLUMNS('m') APPLY(sum) FROM t", + "SELECT JSON_VALUE(doc, '$' DEFAULT (SELECT COLUMNS('m') APPLY(sum) APPLY(max) FROM t) ON EMPTY) FROM src"}) + public void testColumnsInSelectContexts(String sql) throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @ParameterizedTest + @ValueSource(strings = {"COLUMNS('m').x", "{fn COLUMNS('m')}", "COLUMNS('m')(x)"}) + public void testDecoratedColumnsFunctionKeepsAlias(String function) throws JSQLParserException { + String sql = "SELECT " + function + " APPLY(sum) FROM t"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + SelectItem item = select.getSelectItem(0); + Function expression = Assertions.assertInstanceOf(Function.class, item.getExpression()); + Assertions.assertSame(item, expression.getParent()); + Assertions.assertEquals("APPLY", item.getAlias().getName()); + Assertions.assertEquals("sum", item.getAlias().getAliasColumns().get(0).name); + } + + @ParameterizedTest + @ValueSource(strings = {"APPLY(a, b)", "APPLY(a INT)", "REPLACE(a INT)", + "REPLACE(a)", "REPLACE(a, b)"}) + public void testColumnsAliasAstAtEachBoundary(String suffix) throws JSQLParserException { + for (String prefix : new String[] {"", " APPLY(sum)"}) { + String sql = "SELECT COLUMNS('m')" + prefix + " " + suffix + " FROM t"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + SelectItem item = select.getSelectItem(0); + if (prefix.isEmpty()) { + Assertions.assertInstanceOf(Function.class, item.getExpression()); + } else { + ColumnsExpression expression = Assertions.assertInstanceOf(ColumnsExpression.class, + item.getExpression()); + Assertions.assertEquals(1, expression.getTransformers().size()); + Assertions.assertEquals("sum", expression.getTransformers().get(0) + .getApplyExpression().toString()); + } + Alias alias = item.getAlias(); + Assertions.assertNotNull(alias); + Assertions.assertEquals(suffix.substring(0, suffix.indexOf('(')), alias.getName()); + Assertions.assertFalse(alias.isUseAs()); + Assertions.assertEquals("a", alias.getAliasColumns().get(0).name); + if (suffix.contains("INT")) { + Assertions.assertEquals("INT", + alias.getAliasColumns().get(0).colDataType.toString()); + } else { + Assertions.assertNull(alias.getAliasColumns().get(0).colDataType); + if (suffix.contains(",")) { + Assertions.assertEquals("b", alias.getAliasColumns().get(1).name); + } + } + } + } + + @Test + public void testWithLambdaExpressionAliasIssue2632() throws JSQLParserException { + // https://github.com/JSQLParser/JSqlParser/issues/2632 + String sql = "WITH (x -> x * 2) AS double SELECT double(5)"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(sql, true); + WithItem withItem = select.getWithItemsList().get(0); + Assertions.assertTrue(withItem.getExpression() instanceof ParenthesedExpressionList); + LambdaExpression lambda = (LambdaExpression) ((ParenthesedExpressionList) withItem + .getExpression()).get(0); + Assertions.assertEquals(List.of("x"), lambda.getIdentifiers()); + Assertions.assertEquals("double", withItem.getAliasName()); + Assertions.assertEquals("double(5)", + ((PlainSelect) select.getSelectBody()).getSelectItems().get(0).getExpression() + .toString()); + } + + @Test + public void testWithLambdaExpressionAliasDocForms() throws JSQLParserException { + // documented lambda form: (param) -> expr — the AST keeps the parameter name only, + // so the single-parameter parentheses follow the same deparse convention as + // function-argument lambdas and are not preserved + Select select = (Select) CCJSqlParserUtil + .parse("WITH (value) -> value + 1 AS increment SELECT increment(5)"); + LambdaExpression lambda = (LambdaExpression) select.getWithItemsList().get(0) + .getExpression(); + Assertions.assertEquals(List.of("value"), lambda.getIdentifiers()); + Assertions.assertEquals("increment", select.getWithItemsList().get(0).getAliasName()); + assertSqlCanBeParsedAndDeparsed("WITH value -> value + 1 AS increment SELECT increment(5)", + true); + + // multi-parameter lambda with a parenthesized parameter list + assertSqlCanBeParsedAndDeparsed( + "WITH (id, extension) -> concat(lower(id), extension) AS gen_name " + + "SELECT gen_name('A', 'B')", + true); + + // the whole lambda may be wrapped in parentheses (issue #2632 style), multi-param too + assertSqlCanBeParsedAndDeparsed("WITH ((x, y) -> x + y) AS f SELECT f(1, 2)", true); + + // unparenthesized parameter is accepted by ClickHouse as well + assertSqlCanBeParsedAndDeparsed("WITH x -> x * 2 AS double SELECT double(5)", true); + } + + @Test + public void testWithExpressionAlias() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("WITH 1 AS one SELECT one + 1", true); + assertSqlCanBeParsedAndDeparsed("WITH 2 + 2 AS a SELECT a", true); + assertSqlCanBeParsedAndDeparsed( + "WITH '2019-08-01 15:23:00' AS ts_upper_bound SELECT ts_upper_bound", true); + assertSqlCanBeParsedAndDeparsed("WITH sum(number) AS s SELECT s FROM numbers(3)", true); + + // a bare column reference aliased to another name + assertSqlCanBeParsedAndDeparsed("WITH x AS y SELECT y FROM (SELECT 42 AS x)", true); + + // an expression alias may reference an earlier WITH item, and the expression + // may itself be a compound one + assertSqlCanBeParsedAndDeparsed( + "WITH (SELECT sum(number) FROM numbers(10)) AS total, total * 2 AS doubled " + + "SELECT doubled", + true); + assertSqlCanBeParsedAndDeparsed("WITH sum(x) + 1 AS y SELECT y", true); + assertSqlCanBeParsedAndDeparsed( + "WITH CASE WHEN 1 < 2 THEN 'a' ELSE 'b' END AS c SELECT c", true); + assertSqlCanBeParsedAndDeparsed("WITH t AS u, v AS w SELECT u, w", true); + } + + @Test + public void testWithParenthesedSubqueryAlias() throws JSQLParserException { + String sql = "WITH (SELECT a FROM src) AS v SELECT a FROM v, dst"; + assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(Set.of("src", "dst"), TablesNamesFinder.findTables(sql)); + } + + @Test + public void testWithExpressionAliasMixedItems() throws JSQLParserException { + String sql = "WITH 1 AS one, (x -> x * 2) AS double, t AS (SELECT 1 AS a) " + + "SELECT one, double(5), a FROM t"; + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testWithStandardCteUnaffected() throws JSQLParserException { + // guards: the classic CTE shapes must keep parsing exactly as before + assertSqlCanBeParsedAndDeparsed("WITH t AS (SELECT 1 AS a) SELECT a FROM t", true); + assertSqlCanBeParsedAndDeparsed("WITH t (a, b) AS (SELECT 1 AS a, 2 AS b) SELECT a FROM t", + true); + assertSqlCanBeParsedAndDeparsed( + "WITH RECURSIVE q AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM q WHERE n < 3) " + + "SELECT n FROM q", + true); + assertSqlCanBeParsedAndDeparsed( + "WITH a AS MATERIALIZED (SELECT 1 AS b) SELECT b FROM a", true); + assertSqlCanBeParsedAndDeparsed( + "WITH a AS NOT MATERIALIZED (SELECT 1 AS b) SELECT b FROM a", true); + } + + @Test + public void testDataTypeNumericArgsFunctionCall() throws JSQLParserException { + // ClickHouse names lambda aliases after type-like keywords (issue #2632 uses double), + // and calls them like functions: double(5). DATA_TYPE(N) without a trailing literal + // is a function call, not a typed literal. + Select select = (Select) assertSqlCanBeParsedAndDeparsed("SELECT double(5)", true); + Assertions.assertTrue(((PlainSelect) select.getSelectBody()).getSelectItems().get(0) + .getExpression() instanceof Function); + assertSqlCanBeParsedAndDeparsed("SELECT int(5), varchar(5), double(5) + 1", true); + + // the same family: a DATA_TYPE keyword used as a plain column name, e.g. the + // documented ClickHouse corpus sum(number) FROM numbers(10) + assertSqlCanBeParsedAndDeparsed("SELECT number FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT sum(number) FROM numbers(3)", true); + + // guards: typed literals with a trailing literal keep their cast reading + assertSqlCanBeParsedAndDeparsed("SELECT INT '5'", true); + assertSqlCanBeParsedAndDeparsed("SELECT DECIMAL(10,2) '1.5'", true); + } + + @Test + public void testWithExpressionAliasTraversableByVisitorAdapter() throws JSQLParserException { + // a generic SelectVisitorAdapter traversal must reach the expression body of a + // WITH AS item, including nested table references, + // exactly as it reaches the body of a classic CTE + List plainSelects = new ArrayList<>(); + SelectVisitorAdapter visitor = new SelectVisitorAdapter<>() { + @Override + public Void visit(PlainSelect plainSelect, S context) { + plainSelects.add(String.valueOf(plainSelect.getFromItem())); + return super.visit(plainSelect, context); + } + }; + Select select = (Select) CCJSqlParserUtil + .parse("WITH (SELECT 1 FROM src) AS v SELECT a FROM v, dst"); + select.getSelectBody().accept(visitor, null); + Assertions.assertTrue(plainSelects.contains("src"), + "expression body table missing from traversal: " + plainSelects); + + // a lambda alias is handed to the expression visitor + List expressions = new ArrayList<>(); + SelectVisitorAdapter exprVisitor = + new SelectVisitorAdapter<>(new ExpressionVisitorAdapter<>() { + @Override + public Void visit(LambdaExpression lambdaExpression, S context) { + expressions.add(String.valueOf(lambdaExpression)); + return super.visit(lambdaExpression, context); + } + }); + Select lambdaSelect = (Select) CCJSqlParserUtil + .parse("WITH (x -> x * 2) AS double SELECT double(5)"); + Assertions.assertNotNull(lambdaSelect.getWithItemsList().get(0).getExpression()); + lambdaSelect.getSelectBody().accept(exprVisitor, null); + Assertions.assertEquals(List.of("x -> x * 2"), expressions); + } + + @Test + public void testWithExpressionAliasRecursiveClausesSerialize() throws JSQLParserException { + // the tolerant-grammar SEARCH/CYCLE suffix must survive serialization on the + // expression branch instead of being dropped + String sql = "WITH 1 AS one SEARCH DEPTH FIRST BY one SET o SELECT one"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertNotNull(select.getWithItemsList().get(0).getSearchClause()); + + String sql2 = "WITH 1 AS one CYCLE one SET c USING p SELECT one"; + Select select2 = (Select) assertSqlCanBeParsedAndDeparsed(sql2, true); + Assertions.assertNotNull(select2.getWithItemsList().get(0).getCycleClause()); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/CompositeRowExpansionTest.java b/src/test/java/net/sf/jsqlparser/statement/select/CompositeRowExpansionTest.java new file mode 100644 index 0000000000..c2e2a09f8d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/CompositeRowExpansionTest.java @@ -0,0 +1,97 @@ +/*- + * #%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.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * PostgreSQL composite row expansion {@code (function_returning_composite).*}. + * + *

+ * This feature was merged via #2207 and afterwards disabled, because the speculative syntactic + * lookahead used back then caused a severe performance regression. It is re-enabled here through a + * bounded semantic follower check, see {@code FunctionAllColumns} in the grammar. + */ +public class CompositeRowExpansionTest { + + private static FunctionAllColumns assertFunctionAllColumns(String sql) + throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Expression expression = select.getSelectItems().get(0).getExpression(); + Assertions.assertTrue(expression instanceof FunctionAllColumns, + "Expected a FunctionAllColumns select item but got " + expression.getClass()); + return (FunctionAllColumns) expression; + } + + @Test + public void testIssue2412JsonPopulateRecord() throws JSQLParserException { + FunctionAllColumns result = assertFunctionAllColumns( + "SELECT (json_populate_record(NULL::users, data)).* FROM staging_users"); + Assertions.assertEquals("json_populate_record", result.getFunction().getName()); + } + + @Test + public void testSimpleFunctionAllColumns() throws JSQLParserException { + FunctionAllColumns result = assertFunctionAllColumns("SELECT (foo(a, b)).* FROM t"); + Assertions.assertEquals("foo", result.getFunction().getName()); + } + + @Test + public void testPgStatFileExampleFrom2207() throws JSQLParserException { + FunctionAllColumns result = assertFunctionAllColumns( + "SELECT (pg_stat_file('postgresql.conf')).*"); + Assertions.assertEquals("pg_stat_file", result.getFunction().getName()); + } + + @Test + public void testIssue2412InsertSelectUseCase() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO users SELECT (json_populate_record(NULL::users, data)).* FROM staging_users", + true); + } + + @Test + public void testMultipleSurroundingParensAreUnwrapped() throws JSQLParserException { + // Redundant parentheses around a single value are semantically transparent in + // PostgreSQL, so they are unwrapped to the inner function. The round-trip + // therefore normalises to a single surrounding pair. + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT ((((foo(a))))).* FROM t"); + Expression expression = select.getSelectItems().get(0).getExpression(); + Assertions.assertTrue(expression instanceof FunctionAllColumns); + Assertions.assertEquals("foo", ((FunctionAllColumns) expression).getFunction().getName()); + Assertions.assertEquals("(foo(a)).*", expression.toString()); + } + + @Test + public void testParenthesedFunctionWithoutExpansionUnchanged() throws JSQLParserException { + // Without the trailing .* a parenthesised function stays a plain expression. + assertSqlCanBeParsedAndDeparsed("SELECT (foo(a, b)) FROM t", true); + } + + @Test + public void testRowGetExpressionAfterParenthesedFunctionUnchanged() throws JSQLParserException { + // (function()).name must keep parsing as a RowGetExpression, not be swallowed. + assertSqlCanBeParsedAndDeparsed("SELECT (foo(a, b)).colname FROM t", true); + } + + @Test + public void testNonFunctionCompositeExpansionStillUnsupported() { + // Expanding an arbitrary (non-function) expression is out of scope and must + // keep failing cleanly instead of producing a wrong AST. + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT (a + b).* FROM t")); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/DorisJoinHintTest.java b/src/test/java/net/sf/jsqlparser/statement/select/DorisJoinHintTest.java new file mode 100644 index 0000000000..958d2f878c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/DorisJoinHintTest.java @@ -0,0 +1,119 @@ +/*- + * #%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.statement.select; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DorisJoinHintTest { + private static PlainSelect parse(String sql, Dialect dialect) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + @Test + void parsesOriginalReproducerIssue1620() throws Exception { + String sql = "SELECT * FROM uba.events a LEFT JOIN [shuffle] uba.events b " + + "ON a.event_id = b.event_id"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(Dialect.DORIS)); + Join join = select.getJoins().get(0); + assertEquals("shuffle", join.getJoinHint().getKeyword()); + assertEquals(JoinHint.Position.AFTER_JOIN, join.getJoinHint().getPosition()); + assertEquals("uba.events", ((Table) join.getFromItem()).getFullyQualifiedName()); + assertEquals("b", join.getFromItem().getAlias().getName()); + assertEquals(Set.of("uba.events"), new TablesNamesFinder<>().getTables((Statement) select)); + } + + @ParameterizedTest + @ValueSource(strings = {"shuffle", "broadcast", "SHUFFLE", "BROADCAST"}) + void preservesHintPositionAndRoundTrips(String hint) throws Exception { + for (String kind : List.of("", "INNER ", "LEFT OUTER ", "RIGHT ", "LEFT SEMI ")) { + String sql = "SELECT a.id FROM a " + kind + "JOIN [" + hint + "] b USING (id)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(Dialect.DORIS)); + StringBuilder output = new StringBuilder(); + select.accept(new StatementDeParser(output), null); + for (String rendered : List.of(select.toString(), output.toString())) { + JoinHint reparsed = parse(rendered, Dialect.DORIS).getJoins().get(0).getJoinHint(); + assertEquals(hint, reparsed.getKeyword()); + assertEquals(JoinHint.Position.AFTER_JOIN, reparsed.getPosition()); + } + } + } + + @Test + void keepsHintPerJoinAndSupportsAstMutation() throws Exception { + PlainSelect select = parse("SELECT * FROM a JOIN [shuffle] b ON a.id = b.id " + + "JOIN [broadcast] c ON b.id = c.id JOIN d ON c.id = d.id", Dialect.DORIS); + assertEquals("shuffle", select.getJoins().get(0).getJoinHint().getKeyword()); + assertEquals("broadcast", select.getJoins().get(1).getJoinHint().getKeyword()); + assertNull(select.getJoins().get(2).getJoinHint()); + select.getJoins().get(0) + .setJoinHint(new JoinHint("broadcast", JoinHint.Position.AFTER_JOIN)); + TestUtils.assertDeparse(select, "SELECT * FROM a JOIN [broadcast] b ON a.id = b.id " + + "JOIN [broadcast] c ON b.id = c.id JOIN d ON c.id = d.id"); + select.getJoins().get(0).setJoinHint(null); + assertEquals("JOIN b ON a.id = b.id", select.getJoins().get(0).toString()); + } + + @Test + void keepsSqlServerHintsAndQuotedTableNames() throws Exception { + for (String keyword : List.of("LOOP", "HASH", "MERGE", "REMOTE")) { + String sql = "SELECT * FROM a INNER " + keyword + " JOIN b ON a.id = b.id"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + assertEquals(JoinHint.Position.BEFORE_JOIN, + select.getJoins().get(0).getJoinHint().getPosition()); + assertEquals(keyword, new JoinHint(keyword).toString()); + } + PlainSelect quoted = parse("SELECT * FROM a JOIN [shuffle] b ON a.id = b.id", + Dialect.SQLSERVER); + assertNull(quoted.getJoins().get(0).getJoinHint()); + assertEquals("[shuffle]", ((Table) quoted.getJoins().get(0).getFromItem()).getName()); + PlainSelect dorisTable = parse("SELECT * FROM a JOIN `shuffle` b ON a.id = b.id", + Dialect.DORIS); + assertNull(dorisTable.getJoins().get(0).getJoinHint()); + } + + @Test + void requiresDorisDialect() { + String sql = "SELECT * FROM a JOIN [shuffle] db.b b ON a.id = b.id"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.DORIS) { + assertThrows(JSQLParserException.class, () -> parse(sql, dialect), dialect.name()); + } + } + } + + @Test + void rejectsUnknownAndDuplicateHints() { + for (String sql : List.of("SELECT * FROM a JOIN [unknown] b ON a.id = b.id", + "SELECT * FROM a JOIN [shuffle, broadcast] b ON a.id = b.id", + "SELECT * FROM a JOIN [shuffle] [broadcast] b ON a.id = b.id", + "SELECT * FROM a INNER HASH JOIN [shuffle] b ON a.id = b.id", + "SELECT * FROM a JOIN [shuffle b ON a.id = b.id")) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.DORIS), sql); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/DuckDBCompatibilityTest.java b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBCompatibilityTest.java new file mode 100644 index 0000000000..739cc6fc6c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBCompatibilityTest.java @@ -0,0 +1,70 @@ +/*- + * #%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.statement.select; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.MapExpression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; + +class DuckDBCompatibilityTest { + + @Test + void testMapLiteral() throws JSQLParserException { + String sql = "SELECT MAP {'key': 'value', 'key2': 'other'} AS m FROM t"; + + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + MapExpression mapExpression = assertInstanceOf(MapExpression.class, + select.getPlainSelect().getSelectItem(0).getExpression()); + + assertThat(mapExpression.getEntries()).hasSize(2); + assertThat(TablesNamesFinder.findTables(sql)).containsExactly("t"); + } + + @Test + void testCreateViewWithMapLiteral() throws JSQLParserException { + String sql = "CREATE VIEW v AS SELECT " + + "MAP {'category': name, 'count': name} AS summary FROM products"; + + CreateView createView = + (CreateView) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + MapExpression mapExpression = assertInstanceOf(MapExpression.class, + createView.getSelect().getPlainSelect().getSelectItem(0).getExpression()); + + assertThat(mapExpression.getEntries()).hasSize(2); + assertThat(TablesNamesFinder.findTables(sql)).contains("products"); + } + + @Test + void testPivotStatement() throws JSQLParserException { + String sql = "PIVOT sales ON region USING SUM(amount)"; + + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + + assertInstanceOf(PivotQuery.class, statement); + } + + @Test + void testPivotInSubquery() throws JSQLParserException { + String sql = "SELECT * FROM (PIVOT sales ON region USING SUM(amount))"; + + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + ParenthesedSelect parenthesedSelect = + assertInstanceOf(ParenthesedSelect.class, select.getPlainSelect().getFromItem()); + + assertInstanceOf(PivotQuery.class, parenthesedSelect.getSelect()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java index aad68683bb..7d98c6b7c2 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java @@ -10,10 +10,34 @@ package net.sf.jsqlparser.statement.select; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LambdaExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.DescribeStatement; +import net.sf.jsqlparser.statement.PragmaStatement; +import net.sf.jsqlparser.statement.ExtensionStatement; +import net.sf.jsqlparser.statement.AttachStatement; +import net.sf.jsqlparser.statement.DetachStatement; +import net.sf.jsqlparser.statement.ConnectStatement; +import net.sf.jsqlparser.statement.DisconnectStatement; +import net.sf.jsqlparser.statement.PrepareStatement; +import net.sf.jsqlparser.statement.DeallocateStatement; +import net.sf.jsqlparser.statement.CopyStatement; +import net.sf.jsqlparser.statement.create.function.CreateFunction; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.util.Arrays; +import java.util.HashSet; public class DuckDBTest { @@ -36,4 +60,1050 @@ void testCreateWithStruct() throws JSQLParserException { ");"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testGlobOperator() throws JSQLParserException { + String sqlStr = "SELECT * FROM t WHERE b GLOB 'y*'"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testNotGlobOperator() throws JSQLParserException { + String sqlStr = "SELECT * FROM t WHERE b NOT GLOB 'y*'"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testGlobRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT glob FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testSemiJoinWithoutLeft() throws JSQLParserException { + String sqlStr = "SELECT * FROM t SEMI JOIN u USING (id)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Join join = select.getJoins().get(0); + + Assertions.assertTrue(join.isSemi()); + Assertions.assertFalse(join.isLeft()); + Assertions.assertNull(((Table) select.getFromItem()).getAlias()); + } + + @Test + void testAntiJoinWithoutLeft() throws JSQLParserException { + String sqlStr = "SELECT * FROM t ANTI JOIN u ON t.id = u.id"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Join join = select.getJoins().get(0); + + Assertions.assertTrue(join.isAnti()); + Assertions.assertNull(((Table) select.getFromItem()).getAlias()); + } + + @Test + void testLeftAntiJoin() throws JSQLParserException { + String sqlStr = "SELECT * FROM t LEFT ANTI JOIN u ON t.id = u.id"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Join join = select.getJoins().get(0); + + Assertions.assertTrue(join.isLeft()); + Assertions.assertTrue(join.isAnti()); + } + + @Test + void testAntiRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT anti FROM t anti"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testMapColumnType() throws JSQLParserException { + String sqlStr = "CREATE TABLE t (m MAP(VARCHAR, INTEGER))"; + CreateTable createTable = + (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("MAP", + createTable.getColumnDefinitions().get(0).getColDataType().getDataType()); + Assertions.assertEquals(java.util.Arrays.asList("VARCHAR", "INTEGER"), + createTable.getColumnDefinitions().get(0).getColDataType() + .getArgumentsStringList()); + } + + @Test + void testMapCast() throws JSQLParserException { + String sqlStr = "SELECT CAST(m AS MAP(VARCHAR, INTEGER)) FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testDecimalTypeArgumentsStillParse() throws JSQLParserException { + String sqlStr = "CREATE TABLE t (a DECIMAL(10, 2), b VARCHAR(255))"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testInsertByName() throws JSQLParserException { + String sqlStr = "INSERT INTO t BY NAME SELECT 1 AS b, 2 AS a"; + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(Insert.ColumnMatching.BY_NAME, insert.getColumnMatching()); + } + + @Test + void testInsertByPosition() throws JSQLParserException { + String sqlStr = "INSERT INTO t BY POSITION SELECT 1, 2"; + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(Insert.ColumnMatching.BY_POSITION, insert.getColumnMatching()); + } + + @Test + void testInsertWithoutColumnMatching() throws JSQLParserException { + String sqlStr = "INSERT INTO t (a, b) VALUES (1, 2)"; + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(insert.getColumnMatching()); + } + + @Test + void testUsingSamplePercentShorthand() throws JSQLParserException { + String sqlStr = "SELECT * FROM t USING SAMPLE 10%"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + SampleClause sampleClause = select.getFromItem().getSampleClause(); + + Assertions.assertEquals("%", sampleClause.getPercentageUnit()); + Assertions.assertNull(sampleClause.getMethod()); + } + + @Test + void testUsingSamplePercentWithMethodInBrackets() throws JSQLParserException { + String sqlStr = "SELECT * FROM t USING SAMPLE 10 PERCENT (bernoulli)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(SampleClause.SampleMethod.BERNOULLI, + select.getFromItem().getSampleClause().getMethod()); + } + + @Test + void testUsingSampleRowsWithSeed() throws JSQLParserException { + String sqlStr = "SELECT * FROM t USING SAMPLE 10 ROWS (system, 377)"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testUsingSampleReservoir() throws JSQLParserException { + String sqlStr = "SELECT * FROM t USING SAMPLE reservoir (50 ROWS)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(SampleClause.SampleMethod.RESERVOIR, + select.getFromItem().getSampleClause().getMethod()); + } + + @Test + void testDescribeQuery() throws JSQLParserException { + String sqlStr = "DESCRIBE SELECT * FROM t"; + DescribeStatement describe = + (DescribeStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNotNull(describe.getSelect()); + Assertions.assertNull(describe.getTable()); + } + + @Test + void testDescribeQueryTablesNamesFinder() throws JSQLParserException { + String sqlStr = "DESCRIBE SELECT * FROM ds.t"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testDescribeTableStillParses() throws JSQLParserException { + String sqlStr = "DESCRIBE t"; + DescribeStatement describe = + (DescribeStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("t", describe.getTable().getName()); + } + + @Test + void testPragmaWithArguments() throws JSQLParserException { + String sqlStr = "PRAGMA table_info('t')"; + PragmaStatement pragma = + (PragmaStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("table_info", pragma.getName()); + Assertions.assertEquals(1, pragma.getParameters().size()); + } + + @Test + void testPragmaAssignment() throws JSQLParserException { + String sqlStr = "PRAGMA memory_limit = '1GB'"; + PragmaStatement pragma = + (PragmaStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("'1GB'", pragma.getValue().toString()); + } + + @Test + void testPragmaWithoutArguments() throws JSQLParserException { + String sqlStr = "PRAGMA database_list"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPragmaRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT pragma FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testLoadExtension() throws JSQLParserException { + String sqlStr = "LOAD httpfs"; + ExtensionStatement extension = + (ExtensionStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(ExtensionStatement.Operation.LOAD, extension.getOperation()); + Assertions.assertEquals("httpfs", extension.getExtensionName()); + } + + @Test + void testInstallExtension() throws JSQLParserException { + String sqlStr = "INSTALL spatial"; + ExtensionStatement extension = + (ExtensionStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(ExtensionStatement.Operation.INSTALL, extension.getOperation()); + Assertions.assertFalse(extension.isForce()); + } + + @Test + void testForceInstallExtensionFromRepository() throws JSQLParserException { + String sqlStr = "FORCE INSTALL h3 FROM community"; + ExtensionStatement extension = + (ExtensionStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(extension.isForce()); + Assertions.assertEquals("community", extension.getRepository()); + } + + @Test + void testInstallExtensionFromUrl() throws JSQLParserException { + String sqlStr = "INSTALL 'path/to/ext.duckdb_extension'"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testLoadRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT load, install FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testAttachDatabaseWithAlias() throws JSQLParserException { + String sqlStr = "ATTACH 'file.db' AS mydb"; + AttachStatement attach = + (AttachStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("'file.db'", attach.getDatabasePath()); + Assertions.assertEquals("mydb", attach.getAlias()); + } + + @Test + void testAttachDatabaseWithOptions() throws JSQLParserException { + String sqlStr = "ATTACH DATABASE IF NOT EXISTS 'file.db' AS mydb (TYPE SQLITE, READ_ONLY)"; + AttachStatement attach = + (AttachStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(attach.isIfNotExists()); + Assertions.assertTrue(attach.isUsingDatabaseKeyword()); + Assertions.assertEquals(java.util.Arrays.asList("TYPE SQLITE", "READ_ONLY"), + attach.getOptions()); + } + + @Test + void testDetachDatabase() throws JSQLParserException { + String sqlStr = "DETACH mydb"; + DetachStatement detach = + (DetachStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("mydb", detach.getDatabaseName()); + } + + @Test + void testDetachDatabaseIfExists() throws JSQLParserException { + String sqlStr = "DETACH DATABASE IF EXISTS mydb"; + DetachStatement detach = + (DetachStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(detach.isIfExists()); + } + + @Test + void testAttachRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT attach, detach FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testConnectToRemoteDatabase() throws JSQLParserException { + String sqlStr = "CONNECT 'postgres://localhost/mydb'"; + ConnectStatement connect = + (ConnectStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("'postgres://localhost/mydb'", connect.getTarget()); + Assertions.assertNull(connect.getAlias()); + } + + @Test + void testConnectWithAlias() throws JSQLParserException { + String sqlStr = "CONNECT 'postgres://localhost/mydb' AS remote"; + ConnectStatement connect = + (ConnectStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("remote", connect.getAlias()); + } + + @Test + void testDisconnect() throws JSQLParserException { + String sqlStr = "DISCONNECT"; + DisconnectStatement disconnect = + (DisconnectStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(disconnect.getName()); + } + + @Test + void testDisconnectNamedConnection() throws JSQLParserException { + String sqlStr = "DISCONNECT remote"; + DisconnectStatement disconnect = + (DisconnectStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("remote", disconnect.getName()); + } + + @Test + void testConnectByStillParses() throws JSQLParserException { + String sqlStr = "SELECT * FROM t START WITH id = 1 CONNECT BY PRIOR id = parent_id"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPrepareStatement() throws JSQLParserException { + String sqlStr = "PREPARE q AS SELECT * FROM t WHERE a = $1"; + PrepareStatement prepare = + (PrepareStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("q", prepare.getName()); + Assertions.assertNotNull(prepare.getStatement()); + } + + @Test + void testPrepareInsertStatement() throws JSQLParserException { + String sqlStr = "PREPARE ins AS INSERT INTO t VALUES ($1, $2)"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPrepareTablesNamesFinder() throws JSQLParserException { + String sqlStr = "PREPARE q AS SELECT * FROM ds.t"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testDeallocatePreparedStatement() throws JSQLParserException { + String sqlStr = "DEALLOCATE PREPARE q"; + DeallocateStatement deallocate = + (DeallocateStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("q", deallocate.getName()); + Assertions.assertTrue(deallocate.isUsingPrepareKeyword()); + } + + @Test + void testExecutePreparedStatement() throws JSQLParserException { + String sqlStr = "EXECUTE q (1)"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCopyFromFile() throws JSQLParserException { + String sqlStr = "COPY t FROM 'in.csv'"; + CopyStatement copy = + (CopyStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(copy.isFrom()); + Assertions.assertEquals("t", copy.getTable().getName()); + Assertions.assertEquals("'in.csv'", copy.getPath()); + } + + @Test + void testCopyColumnsFromFileWithOptions() throws JSQLParserException { + String sqlStr = "COPY t (a, b) FROM 'in.csv' (AUTO_DETECT true)"; + CopyStatement copy = + (CopyStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(2, copy.getColumns().size()); + Assertions.assertEquals(java.util.Collections.singletonList("AUTO_DETECT true"), + copy.getOptions()); + } + + @Test + void testCopyQueryToFile() throws JSQLParserException { + String sqlStr = + "COPY (SELECT * FROM t) TO 'out.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)"; + CopyStatement copy = + (CopyStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertFalse(copy.isFrom()); + Assertions.assertNotNull(copy.getSelect()); + } + + @Test + void testCopyTableToFile() throws JSQLParserException { + String sqlStr = "COPY t TO 'out.csv' (FORMAT CSV, DELIMITER '|')"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCopyTablesNamesFinder() throws JSQLParserException { + String sqlStr = "COPY ds.t FROM 'in.csv'"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testCreateScalarMacro() throws JSQLParserException { + String sqlStr = "CREATE MACRO add_one(a) AS a + 1"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("add_one", createMacro.getName()); + Assertions.assertEquals(1, createMacro.getParameters().size()); + Assertions.assertFalse(createMacro.isTable()); + } + + @Test + void testCreateOrReplaceTemporaryMacroWithDefault() throws JSQLParserException { + String sqlStr = "CREATE OR REPLACE TEMPORARY MACRO add(a, b := 5) AS a + b"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(createMacro.isOrReplace()); + Assertions.assertTrue(createMacro.isTemporary()); + Assertions.assertEquals("5", + createMacro.getParameters().get(1).getDefaultValue().toString()); + } + + @Test + void testCreateTableMacro() throws JSQLParserException { + String sqlStr = "CREATE MACRO get_t(i) AS TABLE SELECT * FROM t WHERE id = i"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(createMacro.isTable()); + Assertions.assertNotNull(createMacro.getSelect()); + } + + @Test + void testCreateMacroIfNotExists() throws JSQLParserException { + String sqlStr = "CREATE MACRO IF NOT EXISTS add(a, b) AS a + b"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(createMacro.isIfNotExists()); + Assertions.assertEquals("add", createMacro.getName()); + } + + @Test + void testCreateMacroWithTypedParameters() throws JSQLParserException { + String sqlStr = "CREATE MACRO add(a INTEGER, b DOUBLE, c := 5) AS a + b + c"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("INTEGER", createMacro.getParameters().get(0).getType().toString()); + Assertions.assertEquals("DOUBLE", createMacro.getParameters().get(1).getType().toString()); + Assertions.assertNull(createMacro.getParameters().get(2).getType()); + Assertions.assertEquals("5", + createMacro.getParameters().get(2).getDefaultValue().toString()); + } + + @Test + void testCreateOverloadedMacro() throws JSQLParserException { + String sqlStr = "CREATE OR REPLACE MACRO is_maximal(a TINYINT) AS a = 127, " + + "(a INT) AS a = 2147483647"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(2, createMacro.getOverloads().size()); + Assertions.assertEquals("TINYINT", + createMacro.getOverloads().get(0).getParameters().get(0).getType().toString()); + Assertions.assertEquals("a = 2147483647", + createMacro.getOverloads().get(1).getExpression().toString()); + } + + @Test + void testCreateOverloadedTableMacro() throws JSQLParserException { + String sqlStr = "CREATE MACRO get_t(i) AS TABLE SELECT * FROM t WHERE id = i, " + + "(i, j) AS TABLE SELECT * FROM u WHERE id = i AND x = j"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(2, createMacro.getOverloads().size()); + Assertions.assertTrue(createMacro.getOverloads().get(1).isTable()); + Assertions.assertEquals(2, createMacro.getOverloads().get(1).getParameters().size()); + Assertions.assertEquals(new HashSet<>(Arrays.asList("t", "u")), + TablesNamesFinder.findTables(sqlStr)); + } + + @Test + void testCreateFunctionAsMacro() throws JSQLParserException { + for (String sqlStr : Arrays.asList( + "CREATE FUNCTION add(a, b) AS a + b", + "CREATE OR REPLACE TEMPORARY FUNCTION IF NOT EXISTS add(a INTEGER) AS a + 1", + "CREATE FUNCTION get_t(i) AS TABLE SELECT * FROM t WHERE id = i")) { + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(createMacro.isFunctionKeyword()); + } + } + + @Test + void testCreateFunctionOfOtherDialectsIsNotAMacro() throws JSQLParserException { + for (String sqlStr : Arrays.asList( + "CREATE FUNCTION add(a integer, b integer) RETURNS integer AS 'select $1 + $2' LANGUAGE SQL", + "CREATE FUNCTION add(IN a integer, OUT b integer) AS 'select $1 + 1' LANGUAGE SQL", + "CREATE FUNCTION add(IN a integer, OUT b integer) AS $$ select $1 + 1 $$ LANGUAGE SQL", + "CREATE FUNCTION add(IN a integer, OUT b integer) AS $function$ select $1 + 1 $function$ LANGUAGE SQL", + "CREATE OR REPLACE FUNCTION ds.add(x INT64) AS (x + 1) OPTIONS (description = 'd')")) { + Assertions.assertInstanceOf(CreateFunction.class, CCJSqlParserUtil.parse(sqlStr)); + } + } + + @Test + void testCreateExtensionRepository() throws JSQLParserException { + String sqlStr = + "CREATE EXTENSION REPOSITORY my_repo WITH PREFIX 'https://extensions.example.org'"; + CreateExtensionRepository createRepository = + (CreateExtensionRepository) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("my_repo", createRepository.getName()); + Assertions.assertEquals("'https://extensions.example.org'", + createRepository.getPrefix().toString()); + } + + @Test + void testCreateExtensionRepositoryIfNotExists() throws JSQLParserException { + String sqlStr = "CREATE EXTENSION REPOSITORY IF NOT EXISTS my_repo"; + CreateExtensionRepository createRepository = + (CreateExtensionRepository) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(createRepository.isIfNotExists()); + } + + @Test + void testCreatePostgresExtensionStillParses() throws JSQLParserException { + String sqlStr = "CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public CASCADE"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCreateTriggerWithTransitionTablesAndStatementBody() throws JSQLParserException { + String sqlStr = "CREATE TRIGGER trg_audit AFTER UPDATE ON target " + + "REFERENCING OLD TABLE AS o NEW TABLE AS n FOR EACH STATEMENT " + + "INSERT INTO audit SELECT n.id, o.val, n.val FROM o JOIN n ON o.id = n.id"; + CreateTrigger createTrigger = + (CreateTrigger) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(2, createTrigger.getTransitionRelations().size()); + Assertions.assertEquals(CreateTrigger.Orientation.STATEMENT, + createTrigger.getOrientation()); + Assertions.assertNotNull(createTrigger.getBody()); + } + + @Test + void testCreateTriggerBeforeDeleteWithStatementBody() throws JSQLParserException { + String sqlStr = "CREATE TRIGGER trg BEFORE DELETE ON t REFERENCING OLD TABLE AS o " + + "FOR EACH STATEMENT DELETE FROM audit WHERE id IN (SELECT id FROM o)"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPostgreSqlTriggerStillParses() throws JSQLParserException { + String sqlStr = "CREATE TRIGGER trg AFTER UPDATE ON target " + + "REFERENCING OLD TABLE AS o NEW TABLE AS n FOR EACH STATEMENT " + + "EXECUTE FUNCTION f()"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testFromFirstWithSelect() throws JSQLParserException { + String sqlStr = "FROM t SELECT a, b"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(select.isFromFirst()); + Assertions.assertEquals(2, select.getSelectItems().size()); + } + + @Test + void testFromFirstWithWhere() throws JSQLParserException { + String sqlStr = "FROM t WHERE a > 1"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(select.isFromFirst()); + Assertions.assertNull(select.getSelectItems()); + } + + @Test + void testFromFirstWithClauses() throws JSQLParserException { + String sqlStr = "FROM t SELECT a, count(*) WHERE a > 1 GROUP BY a HAVING count(*) > 2 " + + "ORDER BY a LIMIT 10"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testFromFirstWithJoin() throws JSQLParserException { + String sqlStr = "FROM t JOIN u ON t.id = u.id SELECT t.a"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testFromFirstTablesNamesFinder() throws JSQLParserException { + String sqlStr = "FROM ds.t WHERE a > 1"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testBarePipeQueryStillParses() throws JSQLParserException { + String sqlStr = "FROM t |> WHERE a > 1"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testUnPivotStatement() throws JSQLParserException { + String sqlStr = "UNPIVOT monthly_sales ON jan, feb INTO NAME month VALUE sales"; + UnPivotQuery unPivotQuery = + (UnPivotQuery) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("month", unPivotQuery.getNameColumn()); + Assertions.assertEquals(2, unPivotQuery.getOnExpressions().size()); + Assertions.assertEquals(1, unPivotQuery.getValueColumns().size()); + } + + @Test + void testUnPivotStatementWithoutIntoClause() throws JSQLParserException { + String sqlStr = "UNPIVOT monthly_sales ON jan, feb"; + UnPivotQuery unPivotQuery = + (UnPivotQuery) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(unPivotQuery.getNameColumn()); + } + + @Test + void testUnPivotStatementTablesNamesFinder() throws JSQLParserException { + String sqlStr = "UNPIVOT ds.monthly_sales ON jan, feb INTO NAME month VALUE sales"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.monthly_sales"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testUnPivotInFromClauseStillParses() throws JSQLParserException { + String sqlStr = "SELECT * FROM monthly_sales UNPIVOT (sales FOR month IN (jan, feb))"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testApproxNearestJoin() throws JSQLParserException { + String sqlStr = "SELECT * FROM queries q INNER JOIN products t " + + "APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(q.vec, t.vec)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Join join = select.getJoins().get(0); + + Assertions.assertEquals(2L, join.getApproxNearest()); + Assertions.assertEquals("array_cosine_similarity(q.vec, t.vec)", + join.getSimilarity().toString()); + } + + @Test + void testApproxNearestJoinWithoutAlias() throws JSQLParserException { + String sqlStr = "SELECT * FROM queries JOIN products " + + "APPROX NEAREST 5 BY SIMILARITY array_distance(queries.vec, products.vec)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(((Table) select.getJoins().get(0).getFromItem()).getAlias()); + } + + @Test + void testApproxRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT approx, nearest, similarity FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testRecursiveCteUsingKey() throws JSQLParserException { + String sqlStr = "WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS " + + "(SELECT 1, 2 UNION ALL SELECT a + 1, b FROM tbl WHERE a < 3) SELECT * FROM tbl"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + WithItem withItem = select.getWithItemsList().get(0); + + Assertions.assertEquals(2, withItem.getUsingKeyExpressions().size()); + } + + @Test + void testRecursiveCteUsingKeySingleColumn() throws JSQLParserException { + String sqlStr = "WITH RECURSIVE tbl USING KEY (a) AS (SELECT 1 AS a) SELECT * FROM tbl"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testRecursiveCteWithoutUsingKey() throws JSQLParserException { + String sqlStr = "WITH RECURSIVE tbl (a) AS (SELECT 1) SELECT * FROM tbl"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(select.getWithItemsList().get(0).getUsingKeyExpressions()); + } + + @Test + void testLambdaKeyword() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT list_transform(l, lambda x : x + 1) FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT list_filter(l, lambda x : x > 0) FROM t", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT list_filter(l, lambda x : x IS NOT NULL AND x <> 'a') FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT list_reduce(l, lambda acc, x : acc + x) FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT list_filter([(CASE WHEN a THEN 1 END)], lambda x : x IS NOT NULL) FROM t", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT list_transform(l, lambda x : list_filter(x, lambda y : y > 1)) FROM t", + true); + } + + @Test + void testLambdaKeywordExpression() throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT list_reduce(l, lambda acc, x : acc + x) FROM t", true); + Function function = select.getSelectItem(0).getExpression(Function.class); + LambdaExpression lambda = (LambdaExpression) function.getParameters().get(1); + + Assertions.assertTrue(lambda.isLambdaKeyword()); + Assertions.assertEquals(Arrays.asList("acc", "x"), lambda.getIdentifiers()); + } + + @Test + void testLambdaKeywordFollowedByArguments() throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT f(l, lambda x : x + 1, 2) FROM t", true); + Function function = select.getSelectItem(0).getExpression(Function.class); + + Assertions.assertEquals(3, function.getParameters().size()); + Assertions.assertEquals("x + 1", + ((LambdaExpression) function.getParameters().get(1)).getExpression().toString()); + } + + @Test + void testLambdaAsIdentifier() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT lambda FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT lambda x, y FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT t.lambda FROM lambda t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT CAST(a AS lambda) FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT 'a' COLLATE lambda FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a lambda)", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t DROP COLUMN lambda", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t RENAME lambda TO x", true); + } + + @Test + void testFromFirstAndTableAsQueryOperands() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT * FROM (FROM t)", + "SELECT * FROM (FROM t WHERE a > 1) x JOIN (SELECT 1) y ON true", + "SELECT * FROM t WHERE a IN (FROM u)", "SELECT (FROM u SELECT max(a))", + "FROM a UNION ALL FROM b", "FROM a WHERE x > 1 UNION ALL SELECT 1", + "SELECT 1 EXCEPT FROM b WHERE x", "FROM a SELECT x UNION FROM b SELECT x", + "WITH t AS (SELECT 1), u AS (SELECT 2) TABLE t", "TABLE a UNION TABLE b", + "SELECT 1 UNION TABLE b", "(TABLE a EXCEPT ALL TABLE b)", + "SELECT * FROM TABLE(f(1))"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + SetOperationList union = (SetOperationList) CCJSqlParserUtil + .parse("TABLE a UNION TABLE b ORDER BY 1 LIMIT 2"); + Assertions.assertEquals("1", union.getOrderByElements().get(0).toString()); + Assertions.assertNotNull(union.getLimit()); + Assertions.assertNull(((TableStatement) union.getSelect(1)).getOrderByElements()); + + union = (SetOperationList) CCJSqlParserUtil.parse("FROM a UNION FROM b ORDER BY 1"); + Assertions.assertTrue(((PlainSelect) union.getSelect(0)).isFromFirst()); + Assertions.assertEquals("1", union.getOrderByElements().get(0).toString()); + } + + @Test + void testTableQueryKeepsTableColumnsAndTrailingClauses() throws JSQLParserException { + // TABLE also names columns, e.g. in ClickHouse's system.parts + for (String sqlStr : new String[] { + "SELECT database, table FROM system.parts WHERE (database = 'default') AND (table = 'hits')", + "SELECT (table), (table.a), (table + 1) FROM t", + "SELECT a FROM t WHERE (table, b) IN ((1, 2))", + "SELECT * FROM (TABLE s.t) x", "TABLE t FETCH FIRST 1 ROWS ONLY", + "WITH a AS (SELECT 1) TABLE a FETCH FIRST 1 ROWS ONLY", + "CREATE TABLE x AS TABLE a UNION TABLE b"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + Assertions.assertInstanceOf(net.sf.jsqlparser.statement.piped.FromQuery.class, + ((ParenthesedSelect) CCJSqlParserUtil.parse("(FROM t)")).getSelect()); + } + + @Test + void testLimitPercent() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT a FROM t LIMIT 10%", + "SELECT a FROM t LIMIT 10.5%", "SELECT a FROM t LIMIT 40 PERCENT", + "SELECT a FROM t LIMIT 50% OFFSET 2", "SELECT a FROM t LIMIT (SELECT 10)%", + "SELECT * FROM (SELECT a FROM t LIMIT 10%) x", + "SELECT a FROM t LIMIT (SELECT a FROM u LIMIT 5%)", "SELECT a FROM t LIMIT NULL%", + // modulo and a column named offset are unchanged + "SELECT a FROM t LIMIT 10 % 3", "SELECT a FROM t LIMIT a % offset", + "SELECT a FROM t LIMIT a % offset + 1", "SELECT a FROM t LIMIT a % offset OFFSET 2", + "SELECT a FROM t LIMIT (SELECT a % offset + 1 FROM u)"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a FROM t LIMIT 10%"); + Assertions.assertEquals("%", select.getLimit().getPercentUnit()); + Assertions.assertEquals("10", select.getLimit().getRowCount().toString()); + + select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a FROM t LIMIT a % offset + 1"); + Assertions.assertNull(select.getLimit().getPercentUnit()); + Assertions.assertNull(select.getOffset()); + } + + @Test + void testTableSampleWithoutOrWithReservoirMethod() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT a FROM t TABLESAMPLE 10%", + "SELECT a FROM t TABLESAMPLE 10 ROWS", "SELECT a FROM t t1 TABLESAMPLE 10 PERCENT", + "SELECT a FROM t TABLESAMPLE RESERVOIR (10 ROWS) REPEATABLE (1)", + "SELECT a FROM (SELECT 1) TABLESAMPLE 10%", + "SELECT a FROM read_csv('x') TABLESAMPLE 10%", + "SELECT a FROM range(10) r USING SAMPLE 10%"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + // after DuckDB's TABLESAMPLE n, OFFSET belongs to the query + PlainSelect select = (PlainSelect) CCJSqlParserUtil + .parse("SELECT a FROM t TABLESAMPLE 10 OFFSET 3"); + Assertions.assertEquals("3", select.getOffset().getOffset().toString()); + } + + @Test + void testDoubleEqualsOperator() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a FROM t WHERE a == 1 AND b = 2", true); + Assertions.assertEquals("==", + ((net.sf.jsqlparser.expression.operators.relational.EqualsTo) CCJSqlParserUtil + .parseCondExpression("a == 1")).getStringExpression()); + } + + @Test + void testExponentiationOperator() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT 2 ** 3, 2 * 3 ** 2, -2 ** 2", true); + // above the multiplicative operators, left-associative, below the unary sign + Assertions.assertEquals("Multiplication(2, Exponentiation(3, 2))", + tree(CCJSqlParserUtil.parseExpression("2 * 3 ** 2"))); + Assertions.assertEquals("Exponentiation(Exponentiation(2, 3), 2)", + tree(CCJSqlParserUtil.parseExpression("2 ** 3 ** 2"))); + Assertions.assertEquals("Exponentiation(-2, 2)", + tree(CCJSqlParserUtil.parseExpression("-2 ** 2"))); + } + + @Test + void testLikeOperators() throws JSQLParserException { + for (String operator : new String[] {"~~", "~~*", "~~~", "!~~", "!~~*"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a FROM t WHERE a " + operator + " 'x%'", true); + } + net.sf.jsqlparser.expression.operators.relational.LikeExpression like = + (net.sf.jsqlparser.expression.operators.relational.LikeExpression) CCJSqlParserUtil + .parseCondExpression("a !~~* 'x%'"); + Assertions.assertEquals( + net.sf.jsqlparser.expression.operators.relational.LikeExpression.KeyWord.ILIKE, + like.getLikeKeyWord()); + Assertions.assertTrue(like.isNot()); + // the regular expression operators are unchanged + Assertions.assertInstanceOf( + net.sf.jsqlparser.expression.operators.relational.RegExpMatchOperator.class, + CCJSqlParserUtil.parseCondExpression("a !~* 'x'")); + } + + @Test + void testChainedSigns() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT - -1, +-+1, ++-++-+i, ~ ~a, x - -1 FROM t", + true); + Assertions.assertEquals("- - -a", CCJSqlParserUtil.parseExpression("- - -a").toString()); + } + + @Test + void testLikeOperatorFollowsTheModel() throws JSQLParserException { + net.sf.jsqlparser.expression.operators.relational.LikeExpression like = + (net.sf.jsqlparser.expression.operators.relational.LikeExpression) CCJSqlParserUtil + .parseCondExpression("a ~~ 'x'"); + like.setNot(true); + Assertions.assertEquals("a !~~ 'x'", like.toString()); + like.setLikeKeyWord( + net.sf.jsqlparser.expression.operators.relational.LikeExpression.KeyWord.SIMILAR_TO); + Assertions.assertEquals("a NOT SIMILAR TO 'x'", like.toString()); + Assertions.assertThrows(IllegalArgumentException.class, () -> like.setLikeOperator("!~~~")); + Assertions.assertThrows(IllegalArgumentException.class, () -> like.setLikeOperator(null)); + } + + @Test + void testMixedSignsStaySeparate() throws JSQLParserException { + for (String expression : new String[] {"- ~1", "~ -1", "~ - ~a", "- ~ -a", "+ ~a"}) { + String deparsed = CCJSqlParserUtil.parseExpression(expression).toString(); + Assertions.assertEquals(expression.replace(" ", ""), + deparsed.replace(" ", "")); + Assertions.assertFalse(deparsed.contains("-~") || deparsed.contains("~-") + || deparsed.contains("~~") || deparsed.contains("+~"), deparsed); + } + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT - ~1, ~ -1, -+1", true); + } + + private static String tree(net.sf.jsqlparser.expression.Expression expression) { + if (expression instanceof net.sf.jsqlparser.expression.BinaryExpression) { + net.sf.jsqlparser.expression.BinaryExpression binary = + (net.sf.jsqlparser.expression.BinaryExpression) expression; + return expression.getClass().getSimpleName() + "(" + tree(binary.getLeftExpression()) + + ", " + tree(binary.getRightExpression()) + ")"; + } + return expression.toString(); + } + + @Test + void testListComprehension() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT [x * 2 FOR x IN l] FROM t", + "SELECT [x FOR x IN l IF x > 1] FROM t", "SELECT [x + i FOR x, i IN l] FROM t", + "SELECT [[y FOR y IN x] FOR x IN [[1], [2]]]", + "SELECT [x FOR x IN l IF x IN (1, 2)] FROM t", + "SELECT [x FOR x IN (SELECT list(a) FROM t)]", + "SELECT [x FOR x IN l][1] FROM t"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + net.sf.jsqlparser.expression.ListComprehension comprehension = + (net.sf.jsqlparser.expression.ListComprehension) CCJSqlParserUtil + .parseExpression("[x + i FOR x, i IN l IF x > 1]"); + Assertions.assertEquals("x + i", comprehension.getExpression().toString()); + Assertions.assertEquals(Arrays.asList("x", "i"), comprehension.getVariables()); + Assertions.assertEquals("l", comprehension.getList().toString()); + Assertions.assertEquals("x > 1", comprehension.getCondition().toString()); + + Assertions.assertEquals(new HashSet<>(Arrays.asList("t", "u")), + new HashSet<>(new TablesNamesFinder<>().getTables(CCJSqlParserUtil.parse( + "SELECT [x FOR x IN (SELECT list(a) FROM u)] FROM t")))); + } + + @Test + @Timeout(10) + void testLargeArrayIsScannedOnce() throws JSQLParserException { + StringBuilder sql = new StringBuilder("SELECT [0"); + for (int i = 1; i < 20000; i++) { + sql.append(", ").append(i); + } + CCJSqlParserUtil.parse(sql.append("]").toString()); + } + + @Test + void testStarRenameAndUnparenthesizedModifiers() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT * RENAME (i AS x, j AS y) FROM t", + "SELECT * RENAME i AS x FROM t", "SELECT * EXCLUDE i FROM t", + "SELECT * EXCLUDE t.i FROM t", "SELECT * REPLACE i + 1 AS i FROM t", + "SELECT * EXCLUDE (k) RENAME (i AS x) FROM t", + "SELECT t.* RENAME t.i AS x FROM t", + "SELECT COLUMNS(* REPLACE i + 1 AS i) FROM t"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + PlainSelect select = (PlainSelect) CCJSqlParserUtil + .parse("SELECT * RENAME (i AS x) FROM t"); + net.sf.jsqlparser.expression.ColumnsTransformer rename = + ((AllColumns) select.getSelectItem(0) + .getExpression()).getTransformers().get(0); + Assertions.assertEquals( + net.sf.jsqlparser.expression.ColumnsTransformer.ColumnsTransformerType.RENAME, + rename.getType()); + Assertions.assertEquals("i AS x", rename.getRenameItems().get(0).toString()); + + // a star aliased rename or replace keeps parsing + for (String sqlStr : new String[] {"SELECT * rename FROM t", "SELECT * replace FROM t", + "SELECT t.* rename, b FROM t", "SELECT * rename(a, b) FROM t"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + // DuckDB reads a parenthesized expression followed by AS as the single item + select = (PlainSelect) CCJSqlParserUtil.parse("SELECT * REPLACE (a + 1) AS a FROM t"); + Assertions.assertEquals("(a + 1) AS a", + ((AllColumns) select.getSelectItem(0) + .getExpression()).getTransformers().get(0).getReplaceItems().get(0) + .toString()); + } + + @Test + void testTrailingCommaInSelectList() throws JSQLParserException { + Assertions.assertEquals("SELECT i, j FROM t WHERE i > 0", + CCJSqlParserUtil.parse("SELECT i, j, FROM t WHERE i > 0").toString()); + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * RENAME i FROM t")); + Assertions.assertEquals("SELECT 1 LIMIT 1", + CCJSqlParserUtil.parse("SELECT 1, LIMIT 1").toString()); + Assertions.assertEquals("SELECT (SELECT 1)", + CCJSqlParserUtil.parse("SELECT (SELECT 1,)").toString()); + // keywords that may name a column or a function still do + for (String item : new String[] {"limit", "offset", "group", "order", "qualify", + "limit(1)", "offset(1)", "group by", "order by"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a, " + item + " FROM t", true); + } + } + + @Test + void testWindowAndFilterForms() throws JSQLParserException { + for (String sqlStr : new String[] { + "SELECT a FROM t WINDOW w AS (ORDER BY a) QUALIFY row_number() OVER w = 1", + "SELECT a FROM t QUALIFY row_number() OVER w = 1 WINDOW w AS (ORDER BY a)", + "SELECT sum(a) OVER (w ROWS 2 PRECEDING) FROM t WINDOW w AS (ORDER BY b)", + "SELECT sum(a) OVER (w) FROM t WINDOW w AS (ORDER BY b)", + "SELECT sum(a) OVER w2 FROM t WINDOW w AS (PARTITION BY c), w2 AS (w ORDER BY b)", + "SELECT sum(a) OVER (ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t", + "SELECT sum(a) FILTER (b > 1) FROM t", + "SELECT count(*) FILTER (b > 1) OVER () FROM t", + // window specifications inside syntactic lookahead + "SELECT JSON_VALUE(j, '$.a' DEFAULT sum(a) OVER (PARTITION BY b) ON EMPTY) FROM t", + "SELECT JSON_VALUE(j, '$.a' DEFAULT sum(a) OVER (w ORDER BY b) ON EMPTY) FROM t"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT sum(a) FILTER (b > 1) OVER (w ORDER BY c) FROM t WINDOW w AS (PARTITION BY d)"); + net.sf.jsqlparser.expression.AnalyticExpression sum = + (net.sf.jsqlparser.expression.AnalyticExpression) select.getSelectItem(0) + .getExpression(); + Assertions.assertFalse(sum.isFilterWhereKeyword()); + Assertions.assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "SELECT percentile_cont(0.5) WITHIN GROUP (w ORDER BY a) FROM t")); + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT a FROM t ORDER SIBLINGS BY a QUALIFY x = 1")); + // PostgreSQL requires WHERE + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT sum(a) FILTER (b > 1) FROM t", + parser -> parser.withDialect( + net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect.POSTGRESQL))); + Assertions.assertEquals("w", sum.getWindowDefinition().getBaseWindowName()); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapterTest.java b/src/test/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapterTest.java new file mode 100644 index 0000000000..4722220676 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapterTest.java @@ -0,0 +1,150 @@ +/*- + * #%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.statement.select; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.JsonTableFunction; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.StatementFeatureVisitor; +import net.sf.jsqlparser.statement.StatementFeatures; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class FromItemVisitorAdapterTest { + + @Test + void visitsEveryRowsFromFunctionWithTheSameContext() throws JSQLParserException { + PlainSelect select = parse("SELECT * FROM ROWS FROM (f(source_col), g(other_col)) " + + "WITH ORDINALITY AS rows"); + RecordingExpressions expressions = new RecordingExpressions(); + + select.getFromItem().accept(new FromItemVisitorAdapter<>(expressions), + expressions.context); + + assertEquals(List.of("f", "g"), expressions.functions); + assertEquals(List.of("source_col", "other_col"), expressions.columns); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * FROM f(g(source_col))", + "SELECT * FROM (SELECT * FROM f(g(source_col))) nested", + "SELECT * FROM base JOIN f(g(source_col)) AS derived ON 1 = 1" + }) + void selectAdapterUsesTheSuppliedExpressionVisitorForTableFunctions(String sql) + throws JSQLParserException { + RecordingExpressions expressions = new RecordingExpressions(); + SelectVisitorAdapter visitor = new SelectVisitorAdapter<>(expressions); + + parse(sql).accept(visitor, expressions.context); + + assertEquals(List.of("f", "g"), expressions.functions); + assertEquals(List.of("source_col"), expressions.columns); + } + + @Test + void dispatchesJsonTableAndVisitsItsNestedExpressions() throws JSQLParserException { + PlainSelect select = parse("SELECT * FROM JSON_TABLE(payload, '$' " + + "PASSING filter_expr AS filter COLUMNS (" + + "id INT PATH '$.id' DEFAULT fallback_value ON EMPTY, " + + "NESTED PATH '$.items[*]' COLUMNS (" + + "value INT PATH '$.value' DEFAULT nested_default ON ERROR))) AS jt"); + RecordingExpressions expressions = new RecordingExpressions(); + + select.getFromItem().accept(new FromItemVisitorAdapter<>(expressions), + expressions.context); + + assertEquals(1, expressions.jsonTables.size()); + assertSame(((TableFunction) select.getFromItem()).getFunction(), + expressions.jsonTables.get(0)); + assertEquals(List.of("payload", "filter_expr", "fallback_value", "nested_default"), + expressions.columns); + } + + @Test + void traversesSubqueriesInFunctionArgumentsThroughTheConfiguredVisitorChain() + throws JSQLParserException { + RecordingExpressions expressions = new RecordingExpressions(); + SelectVisitorAdapter visitor = new SelectVisitorAdapter<>(expressions); + expressions.setSelectVisitor(visitor); + + parse("SELECT * FROM f((SELECT selected_col FROM g(argument_col)))") + .accept(visitor, expressions.context); + + assertEquals(List.of("f", "g"), expressions.functions); + assertEquals(List.of("selected_col", "argument_col"), expressions.columns); + } + + @Test + void emptyTableFunctionDoesNotInvokeTheExpressionVisitor() { + RecordingExpressions expressions = new RecordingExpressions(); + TableFunction tableFunction = new TableFunction((Function) null); + + assertNull(tableFunction.accept(new FromItemVisitorAdapter<>(expressions), + expressions.context)); + assertEquals(List.of(), expressions.functions); + } + + @Test + void featureAnalysisChecksEachTableFunctionOnce() throws JSQLParserException { + List checkedFunctions = new ArrayList<>(); + StatementFeatures features = StatementFeatureVisitor.analyse( + CCJSqlParserUtil.parse("SELECT * FROM ROWS FROM (f(g(1)), h(2))"), name -> { + checkedFunctions.add(name); + return true; + }); + + assertEquals(List.of("f", "g", "h"), checkedFunctions); + assertFalse(features.mayModifyData()); + } + + private static PlainSelect parse(String sql) throws JSQLParserException { + return ((Select) CCJSqlParserUtil.parse(sql)).getPlainSelect(); + } + + private static final class RecordingExpressions extends ExpressionVisitorAdapter { + private final Object context = new Object(); + private final List columns = new ArrayList<>(); + private final List functions = new ArrayList<>(); + private final List jsonTables = new ArrayList<>(); + + @Override + public Void visit(Column column, S context) { + assertSame(this.context, context); + columns.add(column.getColumnName()); + return super.visit(column, context); + } + + @Override + public Void visit(Function function, S context) { + assertSame(this.context, context); + functions.add(function.getName()); + return super.visit(function, context); + } + + @Override + public Void visit(JsonTableFunction function, S context) { + assertSame(this.context, context); + jsonTables.add(function); + return super.visit(function, context); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/JoinUsingDeParseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/JoinUsingDeParseTest.java new file mode 100644 index 0000000000..4b5f7d33ce --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/JoinUsingDeParseTest.java @@ -0,0 +1,48 @@ +/*- + * #%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.statement.select; + +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +public class JoinUsingDeParseTest { + + private static String usingSelect(int columnCount) { + StringBuilder sb = new StringBuilder("SELECT * FROM a JOIN b USING ("); + for (int i = 0; i < columnCount; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append("c").append(i); + } + return sb.append(")").toString(); + } + + @Test + void deParseOfLargeUsingColumnListIsLinear() throws Exception { + // usingColumns is a LinkedList, so deparsing the clause through + // Select.appendStringListTo used to read it by index (get(i)), which is O(n^2). + String sql = usingSelect(200000); + Statement statement = CCJSqlParserUtil.parse(sql); + assertEquals(sql, assertTimeoutPreemptively(Duration.ofSeconds(5), statement::toString)); + } + + @Test + void deParseOfUsingColumnListIsUnchanged() throws Exception { + String sql = "SELECT * FROM a JOIN b USING (c0, c1, c2)"; + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/KSQLWindowOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/select/KSQLWindowOptionsTest.java new file mode 100644 index 0000000000..e4c682f442 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/KSQLWindowOptionsTest.java @@ -0,0 +1,153 @@ +/*- + * #%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.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.select.KSQLWindow.Duration; +import net.sf.jsqlparser.statement.select.KSQLWindow.TimeUnit; +import net.sf.jsqlparser.statement.select.PlainSelect.EmitMode; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +class KSQLWindowOptionsTest { + private PlainSelect roundTrip(String sql) throws Exception { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql); + StringBuilder builder = new StringBuilder(); + select.accept(new StatementDeParser(builder), null); + assertEquals(select.toString(), builder.toString()); + assertEquals(select.toString(), CCJSqlParserUtil.parse(builder.toString()).toString()); + return select; + } + + @ParameterizedTest + @EnumSource(TimeUnit.class) + void joinsAcceptAllTimeUnitsAndPreserveBrackets(TimeUnit unit) throws Exception { + for (boolean brackets : List.of(false, true)) { + String within = brackets ? "(1 " + unit + ")" : "1 " + unit; + PlainSelect select = roundTrip("SELECT a.id FROM a INNER JOIN b WITHIN " + within + + " GRACE PERIOD 0 SECONDS ON a.id = b.id EMIT CHANGES"); + KSQLJoinWindow window = select.getJoins().get(0).getJoinWindow(); + assertEquals(1, window.getDuration()); + assertEquals(unit, window.getTimeUnit()); + assertEquals(brackets, window.isUsingBrackets()); + assertEquals(0, window.getGracePeriod().getValue()); + assertEquals(TimeUnit.SECONDS, window.getGracePeriod().getTimeUnit()); + assertTrue(select.isEmitChanges()); + } + } + + @ParameterizedTest + @EnumSource(TimeUnit.class) + void aggregationWindowsShareTimeUnitsAndGrace(TimeUnit unit) throws Exception { + for (String shape : List.of("TUMBLING (SIZE 20 ", "SESSION (20 ", + "HOPPING (SIZE 20 ")) { + String spec = shape + unit + + (shape.startsWith("HOPPING") ? ", ADVANCE BY 5 " + unit : "") + + ", GRACE PERIOD 2 " + unit + ")"; + PlainSelect select = + roundTrip("SELECT item_id, SUM(quantity) FROM orders WINDOW " + spec + + " GROUP BY item_id EMIT FINAL LIMIT 10"); + KSQLWindow window = select.getKsqlWindow(); + assertEquals(20, window.getSizeDuration()); + assertEquals(unit, window.getSizeTimeUnit()); + assertEquals(2, window.getGracePeriod().getValue()); + assertEquals(unit, window.getGracePeriod().getTimeUnit()); + assertEquals(EmitMode.FINAL, select.getEmitMode()); + assertFalse(select.isEmitChanges()); + if (shape.startsWith("HOPPING")) { + assertEquals(5, window.getAdvanceDuration()); + assertEquals(unit, window.getAdvanceTimeUnit()); + } + } + } + + @Test + void preservesAsymmetricWindowsAndFollowingJoins() throws Exception { + PlainSelect select = roundTrip( + "SELECT a.id FROM a JOIN b WITHIN (1 HOUR, 5 MINUTES) GRACE PERIOD 2 SECONDS ON a.id = b.id JOIN c WITHIN 10 SECONDS ON c.id = a.id EMIT CHANGES"); + assertEquals(2, select.getJoins().size()); + KSQLJoinWindow window = select.getJoins().get(0).getJoinWindow(); + assertTrue(window.isBeforeAfterWindow()); + assertEquals(1, window.getBeforeDuration()); + assertEquals(TimeUnit.HOUR, window.getBeforeTimeUnit()); + assertEquals(5, window.getAfterDuration()); + assertEquals(TimeUnit.MINUTES, window.getAfterTimeUnit()); + assertNull(select.getJoins().get(1).getJoinWindow().getGracePeriod()); + } + + @Test + void parsesOriginalReproducers() throws Exception { + roundTrip( + "select count(*) from log_data_testtopic_1b138324 t1 inner join TestTopic t2 WITHIN 1 MINUTES on t1.email = t2.email EMIT CHANGES"); + PlainSelect select = roundTrip( + "SELECT count(*), sum('id'), avg('id') FROM TestTopic WINDOW TUMBLING (SIZE 60 SECONDS) EMIT FINAL"); + assertEquals(EmitMode.FINAL, select.getEmitMode()); + } + + @Test + void retainsLegacyBuilderSemanticsAndAllowsMutation() throws Exception { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT * FROM t"); + assertEquals(EmitMode.NONE, select.getEmitMode()); + select.setEmitChanges(true); + assertEquals(EmitMode.CHANGES, select.getEmitMode()); + assertTrue(select.toString().endsWith("EMIT CHANGES")); + select.setEmitMode(EmitMode.FINAL); + assertTrue(select.toString().endsWith("EMIT FINAL")); + select.setEmitChanges(false); + assertEquals("SELECT * FROM t", select.toString()); + KSQLJoinWindow window = new KSQLJoinWindow().withDuration(5).withTimeUnit(TimeUnit.MINUTES); + assertEquals("(5 MINUTES)", window.toString()); + window.withUsingBrackets(false).withGracePeriod(new Duration(0, TimeUnit.SECONDS)); + assertEquals("5 MINUTES GRACE PERIOD 0 SECONDS", window.toString()); + window.setGracePeriod(null); + assertEquals("5 MINUTES", window.toString()); + KSQLWindow aggregate = + new KSQLWindow().withSizeDuration(10).withSizeTimeUnit(TimeUnit.SECONDS) + .withGracePeriod(new Duration(2, TimeUnit.MINUTES)); + assertEquals("TUMBLING (SIZE 10 SECONDS, GRACE PERIOD 2 MINUTES)", aggregate.toString()); + aggregate.setGracePeriod(null); + assertEquals("TUMBLING (SIZE 10 SECONDS)", aggregate.toString()); + assertThrows(IllegalArgumentException.class, () -> new Duration(-1, TimeUnit.SECONDS)); + } + + @Test + void keepsFollowingStatementsAndKeywordIdentifiers() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + "SELECT item_id, COUNT(*) FROM orders WINDOW TUMBLING (SIZE 1 HOUR) GROUP BY item_id EMIT FINAL; SELECT grace, period FROM t;"); + assertEquals(2, statements.size()); + assertEquals("SELECT grace, period FROM t", statements.get(1).toString()); + roundTrip("SELECT * FROM a JOIN b within ON a.id = within.id"); + roundTrip("SELECT * FROM a JOIN b AS within ON a.id = within.id"); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * FROM a JOIN b WITHIN 1 BANANAS ON a.id = b.id", + "SELECT * FROM a JOIN b WITHIN 1 HOURS GRACE 2 MINUTES ON a.id = b.id", + "SELECT * FROM a JOIN b WITHIN 1 HOURS, 2 HOURS ON a.id = b.id", + "SELECT * FROM t WINDOW TUMBLING (SIZE 1 HOUR, GRACE PERIOD -1 SECONDS)", + "SELECT * FROM t WINDOW TUMBLING (SIZE 1 HOUR, GRACE PERIOD 1 SECOND, GRACE PERIOD 2 SECONDS)", + "SELECT * FROM t EMIT FINAL CHANGES"}) + void rejectsMalformedWindowOptions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/LegacyMySqlGroupByTest.java b/src/test/java/net/sf/jsqlparser/statement/select/LegacyMySqlGroupByTest.java new file mode 100644 index 0000000000..d9b91d0673 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/LegacyMySqlGroupByTest.java @@ -0,0 +1,135 @@ +/*- + * #%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.statement.select; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.GroupByDeParser; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.validator.GroupByValidator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +class LegacyMySqlGroupByTest { + private static final Consumer LEGACY = + parser -> parser.withDialect(Dialect.MYSQL).withLegacyMySqlGroupBy(true); + + @ParameterizedTest + @ValueSource(strings = { + "SELECT a FROM b GROUP BY c DESC", + "SELECT COUNT(*) FROM t GROUP BY a ASC, b, c DESC", + "SELECT COUNT(*) FROM t GROUP BY (a + b) DESC, LOWER(c) ASC WITH ROLLUP", + "SELECT COUNT(*) FROM t GROUP BY a DESC HAVING COUNT(*) > 1 ORDER BY a ASC", + "SELECT COUNT(*) FROM t GROUP BY a, b WITH ROLLUP", + "SELECT COUNT(*) FROM t GROUP BY (a, b)" + }) + void parsesAndDeparsesLegacyGrouping(String sql) throws Exception { + for (boolean complex : new boolean[] {false, true}) { + Consumer config = parser -> { + LEGACY.accept(parser); + parser.withAllowComplexParsing(complex); + }; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true, config); + assertEquals(select.toString(), + CCJSqlParserUtil.parse(select.toString(), config).toString()); + } + } + + @Test + void requiresBothMysqlDialectAndExplicitOptIn() { + String sql = "SELECT a FROM b GROUP BY c DESC"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.MYSQL))); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withLegacyMySqlGroupBy(true))); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.MYSQL) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(dialect).withLegacyMySqlGroupBy(true))); + } + } + } + + @Test + void keepsExpressionsVisibleAndDirectionsDistinct() throws Exception { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT COUNT(*) FROM t GROUP BY a ASC, a DESC, b", LEGACY); + GroupByElement group = select.getGroupBy(); + assertInstanceOf(Column.class, group.getGroupByExpressionList().get(0)); + assertEquals(GroupByElement.SortDirection.ASC, group.getGroupBySortDirection(0)); + assertEquals(GroupByElement.SortDirection.DESC, group.getGroupBySortDirection(1)); + assertNull(group.getGroupBySortDirection(2)); + List columns = new ArrayList<>(); + new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + columns.add(column.getColumnName()); + return null; + } + }.visit(group, null); + assertEquals(List.of("a", "a", "b"), columns); + + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("renamed_").append(column.getColumnName()); + } + }; + expressions.setBuilder(output); + new GroupByDeParser(expressions, output).deParse(group); + assertEquals("GROUP BY renamed_a ASC, renamed_a DESC, renamed_b", output.toString()); + + group.addGroupByExpressions(new Column("extra")); + assertEquals("GROUP BY a ASC, a DESC, b, extra", group.toString()); + group.setGroupByExpressions(new ExpressionList<>(new Column("replacement"))); + assertFalse(group.hasGroupBySortDirections()); + assertEquals("GROUP BY replacement", group.toString()); + } + + @Test + void validatesOrderingSeparatelyFromOrdinaryGrouping() throws Exception { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT a FROM t GROUP BY a DESC", LEGACY); + GroupByValidator validator = new GroupByValidator<>(); + validator.setContext(new ValidationContext() + .setCapabilities(Collections.singleton(MySqlVersion.V8_0))); + validator.validate(select.getGroupBy()); + assertTrue(validator.getValidationErrors().values().stream() + .flatMap(errors -> errors.stream()) + .anyMatch(error -> error.getMessage().contains("selectGroupByOrdering"))); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT a FROM t GROUP BY a DESC ASC", + "SELECT a FROM t GROUP BY a DESC,", + "SELECT a FROM t GROUP BY a NULLS FIRST" + }) + void rejectsMalformedOrdering(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, LEGACY)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/MatchRecognizeDialectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/MatchRecognizeDialectTest.java new file mode 100644 index 0000000000..794b1abe62 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/MatchRecognizeDialectTest.java @@ -0,0 +1,210 @@ +/*- + * #%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.statement.select; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.RowPatternFunction; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.StatementFeatureVisitor; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MatchRecognizeDialectTest { + private static String read(String name) throws Exception { + try (InputStream input = MatchRecognizeDialectTest.class.getResourceAsStream( + "/net/sf/jsqlparser/statement/select/match-recognize/" + name)) { + assertNotNull(input, name); + return new String(input.readAllBytes(), StandardCharsets.UTF_8); + } + } + + static Stream executedCases() throws Exception { + return read("execution-cases.txt").lines().map(line -> line.split(" ")) + .map(parts -> Arguments.of(Dialect.valueOf(parts[0]), parts[1])); + } + + private static PlainSelect parse(String sql, Dialect dialect) throws Exception { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + } + + private static String deparse(PlainSelect select) { + StringBuilder builder = new StringBuilder(); + select.accept(new StatementDeParser(builder)); + return builder.toString(); + } + + private static PlainSelect roundTrip(String sql, Dialect dialect) throws Exception { + PlainSelect select = parse(sql, dialect); + assertEquals(select.toString(), deparse(select)); + assertEquals(select.toString(), parse(deparse(select), dialect).toString()); + return select; + } + + @ParameterizedTest(name = "{0}: {1}") + @MethodSource("executedCases") + void executionFixturesRoundTrip(Dialect dialect, String file) throws Exception { + roundTrip(read(file), dialect); + } + + private static String snowflake(String clause) { + return "SELECT * FROM events MATCH_RECOGNIZE (" + clause + ")"; + } + + @ParameterizedTest + @ValueSource(strings = { + "PATTERN (A+) DEFINE A AS price > 0", + "ALL ROWS PER MATCH PATTERN (A+) DEFINE A AS price > 0", + "MEASURES MATCH_SEQUENCE_NUMBER() AS n ALL ROWS PER MATCH PATTERN (A B) DEFINE A AS price > 0", + "MEASURES FINAL LAST(A.price) AS p ALL ROWS PER MATCH OMIT EMPTY MATCHES PATTERN (A*) DEFINE A AS price > 0", + "ALL ROWS PER MATCH SHOW EMPTY MATCHES PATTERN (A*) DEFINE A AS price > 0", + "ALL ROWS PER MATCH WITH UNMATCHED ROWS PATTERN (A+) DEFINE A AS price > 0", + "AFTER MATCH SKIP TO FIRST B PATTERN (A B+) DEFINE A AS price > 0, B AS price < 0", + "AFTER MATCH SKIP TO LAST B PATTERN (A B+) DEFINE A AS price > 0, B AS price < 0", + "AFTER MATCH SKIP TO B PATTERN (A B+) DEFINE A AS price > 0, B AS price < 0", + "PATTERN (PERMUTE(A, B){1,2}) DEFINE A AS price > 0, B AS price < 0", + "PATTERN ({- A+ -} B) DEFINE A AS price > 0, B AS price < 0", + "PATTERN (^ A B | C $) DEFINE A AS price > 0, B AS price = 0, C AS price < 0", + "PATTERN (A+) DEFINE A AS price > LAG(price)", + "MEASURES RUNNING SUM(A.price) + FINAL SUM(A.price) AS total PATTERN (A+) DEFINE A AS price > 0" + }) + void documentedSnowflakeSyntax(String clause) throws Exception { + roundTrip(snowflake(clause), Dialect.SNOWFLAKE); + } + + @Test + void snowflakeDocumentedPrecedenceIsExplicitInTheAst() throws Exception { + String query = snowflake("PATTERN (A B | C) DEFINE A AS price > 0"); + MatchRecognize node = (MatchRecognize) roundTrip(query, Dialect.SNOWFLAKE).getFromItem(); + RowPattern.Operation sequence = (RowPattern.Operation) node.getPattern(); + assertEquals(RowPattern.Operation.Type.SEQUENCE, sequence.getType()); + RowPattern.Operation alternative = (RowPattern.Operation) sequence.getPatterns().get(1); + assertEquals(RowPattern.Operation.Type.ALTERNATION, alternative.getType()); + assertEquals("A (B | C)", sequence.toString()); + MatchRecognize oracle = (MatchRecognize) parse(query, Dialect.ORACLE).getFromItem(); + assertEquals(RowPattern.Operation.Type.ALTERNATION, + ((RowPattern.Operation) oracle.getPattern()).getType()); + assertEquals("(A B) | C", oracle.getPattern().toString()); + } + + @Test + void prefixFunctionsRemainEditableAndVisibleToExistingVisitors() throws Exception { + PlainSelect statement = roundTrip(read("oracle-running-final.sql"), Dialect.ORACLE); + MatchRecognize node = (MatchRecognize) statement.getFromItem(); + Addition sum = (Addition) node.getMeasures().get(0).getExpression(); + RowPatternFunction running = (RowPatternFunction) sum.getLeftExpression(); + RowPatternFunction finalFunction = (RowPatternFunction) sum.getRightExpression(); + assertEquals(RowPatternFunction.EvaluationMode.RUNNING, running.getEvaluationMode()); + assertEquals(RowPatternFunction.EvaluationMode.FINAL, finalFunction.getEvaluationMode()); + running.getFunction().setName("custom_measure"); + running.getFunction().setParameters( + new net.sf.jsqlparser.expression.operators.relational.ExpressionList<>( + new LongValue(42))); + assertTrue(deparse(statement).contains("RUNNING custom_measure(42)")); + assertEquals(statement.toString(), deparse(statement)); + assertTrue(StatementFeatureVisitor.analyse(statement).getUnresolvedReferences() + .contains("custom_measure")); + List columns = new ArrayList<>(); + finalFunction.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + assertEquals("context", context); + columns.add(column.toString()); + return null; + } + }, "context"); + assertEquals(List.of("A.price"), columns); + StringBuilder builder = new StringBuilder(); + finalFunction.accept(new ExpressionDeParser(null, builder) { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("A.changed"); + } + }, null); + assertEquals("FINAL SUM(A.changed)", builder.toString()); + } + + @Test + void subsetNamesAreNotDiscoveredAsTables() throws Exception { + String query = read("oracle-subset.sql"); + net.sf.jsqlparser.expression.Expression expression = + CCJSqlParserUtil.parseExpression("(" + query.replace(";", "") + ")"); + assertFalse(new TablesNamesFinder().getTables(expression).contains("U")); + MatchRecognize node = (MatchRecognize) parse(query, Dialect.ORACLE).getFromItem(); + assertEquals("U", node.getSubsets().get(0).getName()); + assertEquals(List.of("A", "B"), node.getSubsets().get(0).getVariables()); + } + + @Test + void permutationAndExclusionVisitorsKeepStructureWithoutExpansion() throws Exception { + PlainSelect statement = + roundTrip(snowflake("PATTERN (PERMUTE(A, {- B{2} -}, C)) DEFINE A AS price > 0"), + Dialect.ORACLE); + MatchRecognize node = (MatchRecognize) statement.getFromItem(); + RowPattern.Permute permute = (RowPattern.Permute) node.getPattern(); + assertEquals(3, permute.getPatterns().size()); + assertInstanceOf(RowPattern.Exclusion.class, permute.getPatterns().get(1)); + List variables = new ArrayList<>(); + permute.accept(new RowPatternVisitorAdapter() { + @Override + public Void visit(RowPattern.Variable variable, S context) { + assertEquals("pattern", context); + variables.add(variable.getName()); + return null; + } + }, "pattern"); + assertEquals(List.of("A", "B", "C"), variables); + permute.getPatterns().set(2, new RowPattern.Variable("D")); + assertTrue(deparse(statement).contains("PERMUTE(A, {- B{2} -}, D)")); + } + + @Test + void outputPivotAliasAndSampleStayAfterMatchRecognize() throws Exception { + String query = snowflake( + "MEASURES CLASSIFIER() AS label, SUM(A.price) AS total ALL ROWS PER MATCH PATTERN (A+) DEFINE A AS price > 0") + + " PIVOT (SUM(total) FOR label IN ('A')) p SAMPLE (10)"; + PlainSelect statement = roundTrip(query, Dialect.SNOWFLAKE); + MatchRecognize node = (MatchRecognize) statement.getFromItem(); + assertNotNull(node.getPivot()); + assertNotNull(node.getSampleClause()); + List columns = new ArrayList<>(); + node.accept(new FromItemVisitorAdapter(new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + columns.add(column.toString()); + return null; + } + }), null); + assertTrue(columns.containsAll(List.of("total", "label", "A.price"))); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT running, \"final\" FROM t", "SELECT running(price) FROM t", + "SELECT * FROM t FINAL", "SELECT 'RUNNING SUM(x)' FROM t"}) + void prefixWordsKeepExistingUses(String sql) throws Exception { + roundTrip(sql, Dialect.ORACLE); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/MatchRecognizeTest.java b/src/test/java/net/sf/jsqlparser/statement/select/MatchRecognizeTest.java new file mode 100644 index 0000000000..b6c952b87b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/MatchRecognizeTest.java @@ -0,0 +1,300 @@ +/*- + * #%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.statement.select; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.StatementFeatureVisitor; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MatchRecognizeTest { + private static String sql(String pattern) { + return "SELECT * FROM events MATCH_RECOGNIZE (PARTITION BY account_id " + + "ORDER BY seq DESC NULLS LAST MEASURES SUM(A.price) AS total, COUNT(*) AS n " + + "PATTERN (" + pattern + + ") DEFINE A AS price > 0, B AS price = 0, C AS price < 0)"; + } + + private static PlainSelect parse(String sql) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.BIGQUERY)); + } + + private static MatchRecognize match(PlainSelect select) { + return assertInstanceOf(MatchRecognize.class, select.getFromItem()); + } + + private static String deparse(Select select) { + StringBuilder builder = new StringBuilder(); + select.accept(new StatementDeParser(builder)); + return builder.toString(); + } + + private static PlainSelect roundTrip(String sql) throws JSQLParserException { + PlainSelect statement = parse(sql); + String rendered = deparse(statement); + assertEquals(statement.toString(), rendered); + assertEquals(rendered, parse(rendered).toString()); + return statement; + } + + @Test + void reportedQuery() throws Exception { + String input; + try (java.io.InputStream stream = getClass().getResourceAsStream( + "/net/sf/jsqlparser/statement/select/match-recognize-2350.sql")) { + assertNotNull(stream); + input = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + MatchRecognize match = match(roundTrip(input)); + assertEquals(4, match.getMeasures().size()); + assertEquals(3, match.getDefinitions().size()); + assertEquals(Boolean.FALSE, match.getOptions().getUseLongestMatch()); + assertEquals(MatchRecognize.SkipMode.PAST_LAST_ROW, match.getSkipMode()); + assertEquals("low mid+ high+", match.getPattern().toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"A", "A B | C", "(A B | C)+?", "A??", "A*?", "A+?", + "A{3}", "A{2,}", "A{,4}?", "A{2,4}?", "A{@lo,@hi}", "A{?,?}", + "A ()", "| A", "A |", "A||B", "||A||", "^ A+ $", "^A+$$", "A$", "(^)* A", "`$` A", + "(A?)?"}) + void patternRoundTrip(String pattern) throws Exception { + roundTrip(sql(pattern)); + } + + @Test + void precedenceAndMutation() throws Exception { + MatchRecognize match = match(roundTrip(sql("A B | C"))); + RowPattern.Operation alternative = + assertInstanceOf(RowPattern.Operation.class, match.getPattern()); + assertEquals(RowPattern.Operation.Type.ALTERNATION, alternative.getType()); + RowPattern.Operation sequence = + assertInstanceOf(RowPattern.Operation.class, alternative.getPatterns().get(0)); + assertEquals(RowPattern.Operation.Type.SEQUENCE, sequence.getType()); + assertEquals("A", ((RowPattern.Variable) sequence.getPatterns().get(0)).getName()); + sequence.getPatterns().set(1, + new RowPattern.Operation(RowPattern.Operation.Type.ALTERNATION, + List.of(new RowPattern.Variable("B"), new RowPattern.Variable("C")))); + assertEquals("(A (B | C)) | C", match.getPattern().toString()); + roundTrip("SELECT * FROM " + match); + } + + @ParameterizedTest + @ValueSource(strings = {"", " OPTIONS ()", " OPTIONS (use_longest_match = FALSE)", + " OPTIONS (use_longest_match = TRUE)"}) + void optionsPreservePresence(String option) throws Exception { + String query = sql("A"); + MatchRecognize match = + match(roundTrip(query.substring(0, query.length() - 1) + option + ")")); + if (option.isEmpty()) { + assertNull(match.getOptions()); + } else if (option.equals(" OPTIONS ()")) { + assertNull(match.getOptions().getUseLongestMatch()); + } else { + assertEquals(option.contains("TRUE"), match.getOptions().getUseLongestMatch()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"PAST LAST ROW", "TO NEXT ROW"}) + void skip(String skip) throws Exception { + roundTrip(sql("A+").replace(" PATTERN", " AFTER MATCH SKIP " + skip + " PATTERN")); + } + + @Test + void aliasesAndJoinsBindToTheirOwnInput() throws Exception { + String query = sql("A").replace("FROM events", "FROM events AS src") + " AS result"; + MatchRecognize node = match(roundTrip(query)); + assertEquals("src", node.getInput().getAlias().getName()); + assertEquals("result", node.getAlias().getName()); + PlainSelect join = roundTrip(query.replace("FROM events", "FROM accounts x JOIN events") + + " ON x.id = result.n"); + assertInstanceOf(Table.class, join.getFromItem()); + assertInstanceOf(MatchRecognize.class, join.getJoins().get(0).getFromItem()); + node = match(roundTrip(sql("A").replace("events", + "(events JOIN accounts ON events.account_id = accounts.id)"))); + assertInstanceOf(ParenthesedFromItem.class, node.getInput()); + List inputTables = new ArrayList<>(); + node.accept(new FromItemVisitorAdapter() { + @Override + public Void visit(Table table, S context) { + inputTables.add(table.getName()); + return null; + } + }, null); + assertEquals(List.of("events", "accounts"), inputTables); + } + + @Test + void nestedSetOperationLookaheadStillLexesAnchors() throws Exception { + String nested = "SELECT * FROM ((" + sql("^A+$$") + ") UNION ALL (" + sql("A??") + ")) u"; + roundTrip(nested); + } + + @Test + void parameterIndicesExcludeQuantifiers() throws Exception { + PlainSelect select = roundTrip("SELECT ? AS start_param FROM events MATCH_RECOGNIZE (" + + "ORDER BY seq MEASURES COUNT(*) AS n PATTERN (A?? B{?}) " + + "DEFINE A AS price > ?, B AS price < 0) WHERE n > ?"); + MatchRecognize node = match(select); + List indices = new ArrayList<>(); + node.forEachExpression( + expression -> expression.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(JdbcParameter parameter, S context) { + indices.add(parameter.getIndex()); + return null; + } + }, null)); + assertEquals(List.of(2, 3), indices); + RowPattern.Quantified bound = + (RowPattern.Quantified) ((RowPattern.Operation) node.getPattern()).getPatterns() + .get(1); + bound.setLowerBound(new LongValue(4)); + assertTrue(deparse(select).contains("B{4}")); + assertEquals(1, ((JdbcParameter) select.getSelectItem(0).getExpression()).getIndex()); + } + + @Test + void expressionAndSourceCallbacksPreserveContext() throws Exception { + PlainSelect statement = parse(sql("A{@lo,3}")); + List columns = new ArrayList<>(); + Object marker = new Object(); + MatchRecognize node = match(statement); + node.accept(new FromItemVisitorAdapter(new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + assertSame(marker, context); + columns.add(column.toString()); + return null; + } + }) { + @Override + public Void visit(Table table, S context) { + assertSame(marker, context); + columns.add("table:" + table.getName()); + return null; + } + }, marker); + assertEquals( + List.of("table:events", "account_id", "seq", "A.price", "price", "price", "price"), + columns); + StringBuilder builder = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("renamed_").append(column.getColumnName()); + } + }; + expressions.setBuilder(builder); + SelectDeParser selects = new SelectDeParser(expressions, builder); + expressions.setSelectVisitor(selects); + statement.accept((SelectVisitor) selects, marker); + assertTrue(builder.toString().contains("SUM(renamed_price)")); + assertTrue(builder.toString().contains("DEFINE A AS renamed_price > 0")); + assertTrue(builder.toString().contains("ORDER BY renamed_seq DESC NULLS LAST")); + } + + @Test + void discoveryAndFeatureAnalysisReachClauseExpressions() throws Exception { + PlainSelect statement = parse(sql("A").replace("SUM(A.price)", "custom_measure(A.price)") + .replace("A AS price > 0", + "A AS custom_predicate(price) > (SELECT min(limit_value) FROM limits)")); + assertEquals(Set.of("events", "limits"), new TablesNamesFinder() + .getTables((net.sf.jsqlparser.statement.Statement) statement)); + assertTrue(StatementFeatureVisitor.analyse(statement).getUnresolvedReferences() + .containsAll(Set.of("custom_measure", "custom_predicate", "min"))); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT match_recognize FROM t", "SELECT * FROM t match_recognize", + "SELECT * FROM match_recognize", "SELECT match_recognize(price) FROM t", + "SELECT pattern('$$') FROM t", "SELECT $$body$$", "SELECT 'A' || 'B' FROM t", + "SELECT * FROM t WHERE price > ?"}) + void existingNamesAndLiterals(String sql) throws Exception { + roundTrip(sql); + } + + @ParameterizedTest + @ValueSource(strings = {"A{3}?", "^+ A", "A{1+2}", "A{2,3,4}", "A{}", "A{,}", "A**", "{- A }", + "A+++"}) + void rejectsMalformedPatterns(String pattern) { + assertThrows(JSQLParserException.class, () -> parse(sql(pattern))); + } + + @Test + void expressionTableDiscoveryKeepsNestedScopes() throws Exception { + String query = sql("A").replace("SUM(A.price)", + "SUM(A.price) + (SELECT external.price FROM lookup external)"); + net.sf.jsqlparser.expression.Expression expression = + CCJSqlParserUtil.parseExpression("(" + query + ")"); + assertEquals(Set.of("events", "lookup", "external"), + new TablesNamesFinder().getTables(expression)); + } + + @Test + void inputUnionVisitorDoesNotRequirePlainSelect() throws Exception { + MatchRecognize node = match(roundTrip(sql("A").replace("FROM events", + "FROM (SELECT * FROM events UNION ALL SELECT * FROM archive_events)"))); + List tables = new ArrayList<>(); + FromItemVisitorAdapter visitor = new FromItemVisitorAdapter() { + @Override + public Void visit(Table table, S context) { + tables.add(table.getName()); + return null; + } + }; + SelectVisitorAdapter selects = + new SelectVisitorAdapter<>(new ExpressionVisitorAdapter<>(), visitor); + visitor.setSelectVisitor(selects); + node.accept(visitor, null); + assertEquals(List.of("events", "archive_events"), tables); + } + + @Test + void nestedStructRendersEditedExpressionsInsteadOfOriginalTokens() throws Exception { + PlainSelect statement = roundTrip(sql("A").replace("SUM(A.price)", + "ARRAY_AGG(STRUCT(MATCH_ROW_NUMBER() AS row_no, CLASSIFIER() AS label, A.price))")); + net.sf.jsqlparser.expression.Function array = + (net.sf.jsqlparser.expression.Function) match(statement).getMeasures().get(0) + .getExpression(); + net.sf.jsqlparser.expression.StructType struct = + (net.sf.jsqlparser.expression.StructType) array.getParameters().get(0); + struct.getArguments().set(2, new SelectItem<>(new LongValue(99))); + assertTrue(statement.toString().contains("99")); + assertFalse(statement.toString().contains("A.price")); + assertEquals(statement.toString(), deparse(statement)); + roundTrip(statement.toString()); + } + + @Test + void boundsRespectNestingLimit() { + String nested = "(".repeat(12) + "A" + ")".repeat(12); + assertThrows(JSQLParserException.class, () -> parse(sql(nested))); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java new file mode 100644 index 0000000000..7d90bdeb9e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java @@ -0,0 +1,72 @@ +/*- + * #%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.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import org.junit.jupiter.api.Test; + +class MySqlProcedureAnalyseTest { + + @Test + void parseWithoutParameters() throws JSQLParserException { + Select select = (Select) assertSqlCanBeParsedAndDeparsed( + "SELECT col1, col2 FROM heavy_table PROCEDURE ANALYSE()"); + + MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse(); + assertNotNull(procedureAnalyse); + assertNull(procedureAnalyse.getMaxElements()); + assertNull(procedureAnalyse.getMaxMemory()); + } + + @Test + void parseOneParameter() throws JSQLParserException { + Select select = (Select) assertSqlCanBeParsedAndDeparsed( + "SELECT col1 FROM heavy_table PROCEDURE ANALYSE(10)"); + + MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse(); + assertEquals("10", procedureAnalyse.getMaxElements().getStringValue()); + assertNull(procedureAnalyse.getMaxMemory()); + } + + @Test + void parseTwoParametersAfterLimit() throws JSQLParserException { + Select select = (Select) assertSqlCanBeParsedAndDeparsed( + "SELECT col1 FROM heavy_table LIMIT 20 PROCEDURE ANALYSE(10, 256)"); + + MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse(); + assertEquals("10", procedureAnalyse.getMaxElements().getStringValue()); + assertEquals("256", procedureAnalyse.getMaxMemory().getStringValue()); + } + + @Test + void keepAnalyseAndProcedureAvailableAsIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT analyse FROM heavy_table"); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM heavy_table procedure"); + } + + @Test + void rejectInvalidForms() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE(-1)")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE(1, 2, 3)")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYZE()")); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoVariablesTest.java b/src/test/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoVariablesTest.java new file mode 100644 index 0000000000..41acb93216 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoVariablesTest.java @@ -0,0 +1,155 @@ +/*- + * #%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.statement.select; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.UserVariable; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.StatementFeatureVisitor; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementFeatures; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class MySqlSelectIntoVariablesTest { + private static PlainSelect parse(String sql, Dialect dialect) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "MARIADB"}) + void parsesOriginalReproducerAndPreservesTargets(Dialect dialect) throws Exception { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT COUNT(*) INTO @countTotal FROM employee", false, + parser -> parser.withDialect(dialect)); + assertNull(select.getIntoTables()); + MySqlSelectIntoClause into = select.getMySqlSelectIntoClause(); + assertEquals(MySqlSelectIntoClause.Type.VARIABLES, into.getType()); + assertEquals(MySqlSelectIntoClause.Position.BEFORE_FROM, into.getPosition()); + assertEquals("countTotal", into.getVariables().get(0).getName()); + assertEquals(Set.of("employee"), new TablesNamesFinder<>().getTables((Statement) select)); + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "MARIADB"}) + void preservesPositionsQuotedNamesAndRoundTrips(Dialect dialect) throws Exception { + for (String variables : List.of("@a, @b", "@`first name`, @'second name'")) { + for (String sql : List.of("SELECT a, b INTO " + variables + " FROM t", + "SELECT a, b FROM t ORDER BY a LIMIT 1 INTO " + variables, + "SELECT a, b FROM t FOR UPDATE INTO " + variables)) { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, + false, parser -> parser.withDialect(dialect)); + MySqlSelectIntoClause into = select.getMySqlSelectIntoClause(); + assertEquals(sql.endsWith("FROM t") ? MySqlSelectIntoClause.Position.BEFORE_FROM + : MySqlSelectIntoClause.Position.TRAILING, into.getPosition()); + assertEquals(variables, into.getVariables().toString()); + StringBuilder output = new StringBuilder(); + select.accept(new StatementDeParser(output), null); + for (String rendered : List.of(select.toString(), output.toString())) { + MySqlSelectIntoClause reparsed = + parse(rendered, dialect).getMySqlSelectIntoClause(); + assertEquals(into.getPosition(), reparsed.getPosition()); + assertEquals(variables, reparsed.getVariables().toString()); + } + } + } + assertEquals("@value", parse("SELECT 1 INTO @value", dialect) + .getMySqlSelectIntoClause().getVariables().toString()); + } + + @Test + void requiresMySqlDialectAndKeepsTableTargets() throws Exception { + String sql = "SELECT a INTO @value FROM t"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.MYSQL && dialect != Dialect.MARIADB) { + assertThrows(JSQLParserException.class, () -> parse(sql, dialect), dialect.name()); + } + } + PlainSelect tableInto = parse("SELECT a INTO target FROM source", Dialect.SQLSERVER); + assertNull(tableInto.getMySqlSelectIntoClause()); + assertEquals("target", tableInto.getIntoTables().get(0).getName()); + } + + @Test + void visitsAndRewritesVariables() throws Exception { + PlainSelect select = parse("SELECT a, b FROM t INTO @x, @y", Dialect.MYSQL); + List names = new ArrayList<>(); + select.accept(new SelectVisitorAdapter(new ExpressionVisitorAdapter() { + @Override + public Void visit(UserVariable variable, S context) { + names.add(variable.getName()); + return null; + } + }), null); + assertEquals(List.of("x", "y"), names); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(UserVariable variable, S context) { + return getBuilder().append("@renamed_").append(variable.getName()); + } + }; + SelectDeParser selects = new SelectDeParser(expressions, output); + expressions.setSelectVisitor(selects); + expressions.setBuilder(output); + select.accept((SelectVisitor) selects, null); + assertEquals("SELECT a, b FROM t INTO @renamed_x, @renamed_y", output.toString()); + select.getMySqlSelectIntoClause().getVariables().get(0).setName("new_x"); + TestUtils.assertDeparse(select, "SELECT a, b FROM t INTO @new_x, @y"); + } + + @Test + void reportsSessionAssignmentInsteadOfTableCreationOrResultSet() throws Exception { + StatementFeatures features = StatementFeatureVisitor.analyse( + parse("SELECT a INTO @value FROM t", Dialect.MYSQL)); + assertTrue(features.is(StmtFeature.READS_DATA)); + assertTrue(features.is(StmtFeature.MODIFIES_SESSION)); + assertFalse(features.modifiesSchema()); + assertFalse(features.modifiesData()); + assertFalse(features.returnsResultSet()); + } + + @Test + void rejectsSystemVariablesMalformedListsAndDuplicateInto() { + for (String sql : List.of("SELECT 1 INTO @@sql_mode", "SELECT 1 INTO @", + "SELECT a, b INTO @x, FROM t", "SELECT a INTO @x + 1 FROM t", + "SELECT a INTO @x FROM t INTO @y", "SELECT a INTO @x INTO target FROM t", + "SELECT a INTO target FROM t INTO @x", + "SELECT a INTO @x FROM t INTO OUTFILE '/tmp/result'")) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL), sql); + } + } + + @Test + void keepsStatementBoundaries() throws Exception { + assertEquals(2, CCJSqlParserUtil.parseStatements( + "SELECT a INTO @x FROM t; SELECT @x;", + parser -> parser.withDialect(Dialect.MYSQL)).size()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/MySqlSelectModifierTest.java b/src/test/java/net/sf/jsqlparser/statement/select/MySqlSelectModifierTest.java new file mode 100644 index 0000000000..84424742a8 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/MySqlSelectModifierTest.java @@ -0,0 +1,211 @@ +/*- + * #%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.statement.select; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class MySqlSelectModifierTest { + private static PlainSelect parse(String sql, Dialect dialect) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + @ParameterizedTest + @EnumSource(MySqlSelectModifier.class) + void separatesModifiersFromColumnsAndAliases(MySqlSelectModifier modifier) throws Exception { + String sql = "SELECT " + modifier + " a FROM t"; + for (Dialect dialect : List.of(Dialect.MYSQL, Dialect.MARIADB)) { + PlainSelect select = parse(sql, dialect); + assertEquals(Set.of(modifier), select.getMySqlSelectModifiers()); + SelectItem item = select.getSelectItem(0); + assertEquals("a", ((Column) item.getExpression()).getColumnName()); + assertNull(item.getAlias()); + assertEquals(Set.of("t"), new TablesNamesFinder<>().getTables((Statement) select)); + assertRoundTrip(select, sql, dialect); + } + PlainSelect generic = (PlainSelect) CCJSqlParserUtil.parse(sql); + assertEquals(Set.of(modifier), generic.getMySqlSelectModifiers()); + assertNull(generic.getSelectItem(0).getAlias()); + } + + @Test + void combinesDistinctAndDocumentedModifiers() throws Exception { + String sql = "SELECT DISTINCT HIGH_PRIORITY STRAIGHT_JOIN SQL_SMALL_RESULT SQL_BIG_RESULT " + + "SQL_BUFFER_RESULT SQL_NO_CACHE SQL_CALC_FOUND_ROWS a FROM t LIMIT 1"; + PlainSelect select = parse(sql, Dialect.MYSQL); + EnumSet expected = EnumSet.allOf(MySqlSelectModifier.class); + expected.remove(MySqlSelectModifier.SQL_CACHE); + assertEquals(expected, select.getMySqlSelectModifiers()); + assertTrue(select.getMySqlHintStraightJoin()); + assertTrue(select.getMySqlSqlCalcFoundRows()); + assertEquals(MySqlSqlCacheFlags.SQL_NO_CACHE, select.getMySqlSqlCacheFlag()); + assertRoundTrip(select, sql, Dialect.MYSQL); + assertRoundTrip(parse("SELECT ALL HIGH_PRIORITY SQL_BUFFER_RESULT a FROM t", Dialect.MYSQL), + "SELECT HIGH_PRIORITY SQL_BUFFER_RESULT a FROM t", Dialect.MYSQL); + } + + @Test + void acceptsModifiersBeforeAndAfterDistinctOrAll() throws Exception { + for (String sql : List.of( + "SELECT HIGH_PRIORITY DISTINCT SQL_SMALL_RESULT SQL_BUFFER_RESULT SQL_NO_CACHE a FROM t", + "SELECT SQL_SMALL_RESULT HIGH_PRIORITY DISTINCT SQL_BUFFER_RESULT SQL_NO_CACHE a FROM t", + "SELECT DISTINCT HIGH_PRIORITY SQL_SMALL_RESULT SQL_BUFFER_RESULT SQL_NO_CACHE a FROM t")) { + PlainSelect select = parse(sql, Dialect.MYSQL); + assertTrue(select.getDistinct() != null); + assertNull(select.getSelectItem(0).getAlias()); + assertRoundTrip(select, + "SELECT DISTINCT HIGH_PRIORITY SQL_SMALL_RESULT SQL_BUFFER_RESULT SQL_NO_CACHE a FROM t", + Dialect.MYSQL); + } + assertRoundTrip(parse("SELECT SQL_SMALL_RESULT ALL a FROM t", Dialect.MYSQL), + "SELECT SQL_SMALL_RESULT a FROM t", Dialect.MYSQL); + } + + @Test + void preservesLegacyAccessorsAndBuilders() throws Exception { + PlainSelect select = parse("SELECT STRAIGHT_JOIN SQL_CACHE SQL_CALC_FOUND_ROWS a FROM t", + Dialect.MYSQL); + select.setMySqlSqlCacheFlag(MySqlSqlCacheFlags.SQL_NO_CACHE); + assertFalse(select.getMySqlSelectModifiers().contains(MySqlSelectModifier.SQL_CACHE)); + select.setMySqlHintStraightJoin(false); + select.setMySqlSqlCalcFoundRows(false); + assertEquals(Set.of(MySqlSelectModifier.SQL_NO_CACHE), select.getMySqlSelectModifiers()); + select.setMySqlSqlCacheFlag(null); + assertTrue(select.getMySqlSelectModifiers().isEmpty()); + select.withMySqlHintStraightJoin(true).withMySqlSqlCalcFoundRows(true) + .withMySqlSqlNoCache(MySqlSqlCacheFlags.SQL_CACHE) + .addMySqlSelectModifiers(MySqlSelectModifier.HIGH_PRIORITY); + assertRoundTrip(select, + "SELECT HIGH_PRIORITY STRAIGHT_JOIN SQL_CACHE SQL_CALC_FOUND_ROWS a FROM t", + Dialect.MYSQL); + select.setMySqlSelectModifiers(select.getMySqlSelectModifiers()); + assertEquals(4, select.getMySqlSelectModifiers().size()); + select.withMySqlSelectModifiers(Set.of(MySqlSelectModifier.SQL_BIG_RESULT)); + assertFalse(select.getMySqlHintStraightJoin()); + assertFalse(select.getMySqlSqlCalcFoundRows()); + assertNull(select.getMySqlSqlCacheFlag()); + select.setMySqlSelectModifiers(null); + assertTrue(select.getMySqlSelectModifiers().isEmpty()); + select.setMySqlSqlCacheFlag(MySqlSqlCacheFlags.SQL_CACHE); + assertThrows(IllegalArgumentException.class, + () -> select.addMySqlSelectModifiers(MySqlSelectModifier.SQL_NO_CACHE)); + assertEquals(Set.of(MySqlSelectModifier.SQL_CACHE), select.getMySqlSelectModifiers()); + } + + @Test + void keepsOtherDialectAndQuotedOrQualifiedIdentifiers() throws Exception { + for (String modifier : List.of("HIGH_PRIORITY", "SQL_SMALL_RESULT", "SQL_BIG_RESULT", + "SQL_BUFFER_RESULT")) { + for (Dialect dialect : List.of(Dialect.POSTGRESQL, Dialect.ORACLE, Dialect.SQLSERVER)) { + PlainSelect select = parse("SELECT " + modifier + " a FROM t", dialect); + assertTrue(select.getMySqlSelectModifiers().isEmpty()); + assertEquals(modifier, + ((Column) select.getSelectItem(0).getExpression()).getColumnName()); + assertEquals("a", select.getSelectItem(0).getAlias().getName()); + } + for (String expression : List.of("`" + modifier + "`", "t." + modifier, + modifier + ".a")) { + PlainSelect select = parse("SELECT " + expression + " FROM t", Dialect.MYSQL); + assertTrue(select.getMySqlSelectModifiers().isEmpty()); + assertEquals(expression, select.getSelectItem(0).getExpression().toString()); + } + } + PlainSelect identifier = parse("SELECT SQL_BUFFER_RESULT FROM t", Dialect.MYSQL); + assertTrue(identifier.getMySqlSelectModifiers().isEmpty()); + assertEquals("SQL_BUFFER_RESULT", + ((Column) identifier.getSelectItem(0).getExpression()).getColumnName()); + } + + @Test + void visitsAndRewritesActualProjectionWithoutTreatingModifiersAsColumns() throws Exception { + PlainSelect select = parse("SELECT HIGH_PRIORITY SQL_SMALL_RESULT a FROM t", Dialect.MYSQL); + List columns = new ArrayList<>(); + select.accept(new SelectVisitorAdapter(new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + columns.add(column.getColumnName()); + return null; + } + }), null); + assertEquals(List.of("a"), columns); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("renamed_").append(column.getColumnName()); + } + }; + SelectDeParser selects = new SelectDeParser(expressions, output); + expressions.setSelectVisitor(selects); + expressions.setBuilder(output); + select.accept((SelectVisitor) selects, null); + assertEquals("SELECT HIGH_PRIORITY SQL_SMALL_RESULT renamed_a FROM t", output.toString()); + } + + @Test + void keepsNestedQueriesAndStatementBoundaries() throws Exception { + String sql = "SELECT SQL_SMALL_RESULT a FROM " + + "(SELECT SQL_BIG_RESULT a FROM t) s"; + PlainSelect outer = parse(sql, Dialect.MYSQL); + PlainSelect inner = ((ParenthesedSelect) outer.getFromItem()).getPlainSelect(); + assertEquals(Set.of(MySqlSelectModifier.SQL_SMALL_RESULT), outer.getMySqlSelectModifiers()); + assertEquals(Set.of(MySqlSelectModifier.SQL_BIG_RESULT), inner.getMySqlSelectModifiers()); + assertRoundTrip(outer, sql, Dialect.MYSQL); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT HIGH_PRIORITY b FROM u;", + parser -> parser.withDialect(Dialect.MYSQL)).size()); + } + + @Test + void rejectsConflictingCacheModesAndDistinctQualifiers() { + for (String sql : List.of("SELECT SQL_CACHE SQL_NO_CACHE a FROM t", + "SELECT SQL_NO_CACHE SQL_CACHE a FROM t", + "SELECT DISTINCT ALL a FROM t", "SELECT ALL DISTINCT a FROM t", + "SELECT ALL HIGH_PRIORITY DISTINCT a FROM t", + "SELECT HIGH_PRIORITY DISTINCT SQL_SMALL_RESULT ALL a FROM t")) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL), sql); + } + } + + private static void assertRoundTrip(PlainSelect select, String expected, Dialect dialect) + throws Exception { + assertEquals(expected, select.toString()); + StringBuilder output = new StringBuilder(); + select.accept(new StatementDeParser(output), null); + assertEquals(expected, output.toString()); + for (String rendered : List.of(select.toString(), output.toString())) { + PlainSelect reparsed = parse(rendered, dialect); + assertEquals(select.getMySqlSelectModifiers(), reparsed.getMySqlSelectModifiers()); + assertEquals(select.getSelectItems().toString(), reparsed.getSelectItems().toString()); + } + TestUtils.assertDeparse(select, expected); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java b/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java index fc5bd785d7..37a4ef87bc 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java @@ -15,6 +15,8 @@ import org.junit.jupiter.api.Timeout; import java.util.logging.Logger; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import org.junit.jupiter.api.Assertions; @@ -139,22 +141,23 @@ public void testRecursiveBracketExpressionIssue1019_2() throws JSQLParserExcepti doIncreaseOfParseTimeTesting("IF(1=1, $1, 2)", "1", 20); } - @Test void testIssue2422() throws JSQLParserException { + @Test + void testIssue2422() throws JSQLParserException { String sqlStr = "SELECT\n" - + "\t\t\t\t ((((position('-' IN (\n" - + "\t\t\t\t CASE WHEN ((\n" - + "\t\t\t\t CASE WHEN (5 < 0) THEN\n" - + "\t\t\t\t 'yes'\n" - + "\t\t\t\t ELSE\n" - + "\t\t\t\t 'no'\n" - + "\t\t\t\t END) = 'yes') THEN\n" - + "\t\t\t\t SUBSTRING('2012-january-18', (((LENGTH('2012-january-18')) + (5)) + (1)), ABS((0) - (5)))\n" - + "\t\t\t\t ELSE\n" - + "\t\t\t\t SUBSTRING('2012-january-18', ((5) + (1)))\n" - + "\t\t\t\t END)) - 1) + (1)) - (5)) + (0))\n" - + "\t\t\t\tFROM\n" - + "\t\t\t\t testtable"; + + "\t\t\t\t ((((position('-' IN (\n" + + "\t\t\t\t CASE WHEN ((\n" + + "\t\t\t\t CASE WHEN (5 < 0) THEN\n" + + "\t\t\t\t 'yes'\n" + + "\t\t\t\t ELSE\n" + + "\t\t\t\t 'no'\n" + + "\t\t\t\t END) = 'yes') THEN\n" + + "\t\t\t\t SUBSTRING('2012-january-18', (((LENGTH('2012-january-18')) + (5)) + (1)), ABS((0) - (5)))\n" + + "\t\t\t\t ELSE\n" + + "\t\t\t\t SUBSTRING('2012-january-18', ((5) + (1)))\n" + + "\t\t\t\t END)) - 1) + (1)) - (5)) + (0))\n" + + "\t\t\t\tFROM\n" + + "\t\t\t\t testtable"; assertSqlCanBeParsedAndDeparsed(sqlStr); } @@ -204,18 +207,17 @@ public void testIncreaseOfParseTime() throws JSQLParserException { private void doIncreaseOfParseTimeTesting(String template, String finalExpression, int maxDepth) throws JSQLParserException { - long oldDurationTime = 2000; + long oldDurationTime = TimeUnit.SECONDS.toNanos(2); int countProblematic = 0; for (int i = 0; i < maxDepth; i++) { String sql = "SELECT " + buildRecursiveBracketExpression(template, finalExpression, i) + " FROM mytbl"; - long startTime = System.currentTimeMillis(); - assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withTimeOut(12000)); - long durationTime = System.currentTimeMillis() - startTime; + long durationTime = medianParseDuration(sql); if (i > 0) { - System.out.println("old duration " + oldDurationTime + " new duration time " - + durationTime + " for " + sql); + System.out.println("old duration " + TimeUnit.NANOSECONDS.toMicros(oldDurationTime) + + " us, new duration " + TimeUnit.NANOSECONDS.toMicros(durationTime) + + " us for " + sql); } if (oldDurationTime * 10 < durationTime) { countProblematic++; @@ -228,6 +230,19 @@ private void doIncreaseOfParseTimeTesting(String template, String finalExpressio } } + private long medianParseDuration(String sql) throws JSQLParserException { + // A single millisecond sample can turn scheduler or GC jitter into a tenfold increase. + // Keep the growth limit, but compare representative durations from a monotonic clock. + long[] durations = new long[5]; + for (int sample = 0; sample < durations.length; sample++) { + long startTime = System.nanoTime(); + assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withTimeOut(12000)); + durations[sample] = System.nanoTime() - startTime; + } + Arrays.sort(durations); + return durations[durations.length / 2]; + } + @Test @Timeout(2000) public void testRecursiveBracketExpression() { @@ -552,8 +567,7 @@ JSQLParserException.class, new Executable() { public void execute() throws Throwable { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } - } - ); + }); } @Test @@ -566,8 +580,7 @@ JSQLParserException.class, new Executable() { public void execute() throws Throwable { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } - } - ); + }); } @Test diff --git a/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java new file mode 100644 index 0000000000..113d9235f2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java @@ -0,0 +1,219 @@ +/*- + * #%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.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.merge.Merge; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class OptionClauseTest { + + @Test + public void testOptionHintIssue161() throws JSQLParserException { + String sql = + "SELECT CustomerID, PersonID, StoreID FROM cte OPTION (MAXRECURSION 2)"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + PlainSelect plainSelect = (PlainSelect) ((Select) statement).getPlainSelect(); + Assertions.assertNotNull(plainSelect.getOption()); + Assertions.assertEquals(1, plainSelect.getOption().getOptionHints().size()); + Assertions.assertEquals("MAXRECURSION", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("2", + plainSelect.getOption().getOptionHints().get(0).getValue().toString()); + } + + @Test + public void testOptionWithoutArguments() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE x = 1 OPTION (RECOMPILE)", true); + } + + @Test + public void testOptionMultiWordHints() throws JSQLParserException { + String sql = + "SELECT * FROM t OPTION (KEEP PLAN, KEEPFIXED PLAN, FORCE ORDER, EXPAND VIEWS, NO_PERFORMANCE_SPOOL, IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(6, plainSelect.getOption().getOptionHints().size()); + Assertions.assertEquals("KEEP PLAN", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("FORCE ORDER", + plainSelect.getOption().getOptionHints().get(2).getName()); + } + + @Test + public void testOptionJoinAndUnionHints() throws JSQLParserException { + String sql = "SELECT * FROM t OPTION (HASH JOIN, LOOP JOIN, MERGE JOIN, CONCAT UNION)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(4, plainSelect.getOption().getOptionHints().size()); + Assertions.assertEquals("HASH JOIN", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("CONCAT UNION", + plainSelect.getOption().getOptionHints().get(3).getName()); + } + + @Test + public void testOptionNumericValueHints() throws JSQLParserException { + String sql = "SELECT * FROM t OPTION (FAST 100, MAXDOP 4, MAXRECURSION 0)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals("FAST", plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("100", + plainSelect.getOption().getOptionHints().get(0).getValue().toString()); + Assertions.assertFalse(plainSelect.getOption().getOptionHints().get(0).isUseEquals()); + } + + @Test + public void testOptionEqualsValueHints() throws JSQLParserException { + String sql = "SELECT * FROM t OPTION (MAX_GRANT_PERCENT = 25.5, MIN_GRANT_PERCENT = 10)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals("MAX_GRANT_PERCENT", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertTrue(plainSelect.getOption().getOptionHints().get(0).isUseEquals()); + Assertions.assertEquals("25.5", + plainSelect.getOption().getOptionHints().get(0).getValue().toString()); + } + + @Test + public void testOptionParameterizedHints() throws JSQLParserException { + String sql = + "SELECT * FROM t OPTION (OPTIMIZE FOR UNKNOWN, USE HINT ('ASSUME_JOIN_PREDICATE_DEPENDS_ON_COLUMNS', 'DISABLE_PARAMETER_SNIFFING'))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals("OPTIMIZE FOR UNKNOWN", + plainSelect.getOption().getOptionHints().get(0).getName()); + OptionHint useHint = plainSelect.getOption().getOptionHints().get(1); + Assertions.assertEquals("USE HINT", useHint.getName()); + Assertions.assertEquals(2, useHint.getParameters().size()); + } + + @Test + public void testOptionOptimizeForAssignments() throws JSQLParserException { + String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p = 1))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals("OPTIMIZE FOR", optimizeFor.getName()); + Assertions.assertEquals(1, optimizeFor.getParameters().size()); + Assertions.assertEquals("@p = 1", optimizeFor.getParameters().get(0).toString()); + } + + @Test + public void testOptionTableHint() throws JSQLParserException { + String sql = "SELECT * FROM t1 JOIN t2 ON t1.a = t2.a OPTION (TABLE HINT (t2, INDEX(ix2)))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint tableHint = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals("TABLE HINT", tableHint.getName()); + Assertions.assertEquals("t2", tableHint.getParameters().get(0).toString()); + Assertions.assertEquals("INDEX(ix2)", tableHint.getParameters().get(1).toString()); + } + + @Test + public void testOptionAfterUnion() throws JSQLParserException { + String sql = + "SELECT a FROM t1 UNION SELECT a FROM t2 ORDER BY a OPTION (RECOMPILE, MAXDOP 1) LIMIT 10"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + SetOperationList setOperationList = + (SetOperationList) ((Select) statement).getSelectBody(); + Assertions.assertNotNull(setOperationList.getOption()); + Assertions.assertEquals(2, setOperationList.getOption().getOptionHints().size()); + } + + @Test + public void testOptionAfterForXml() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t FOR XML PATH('row') OPTION (RECOMPILE)", true); + } + + @Test + public void testOptionInUpdateAndDelete() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("UPDATE t SET a = 1 WHERE b = 2 OPTION (RECOMPILE)", true); + assertSqlCanBeParsedAndDeparsed("DELETE FROM t WHERE a = 1 OPTION (LOOP JOIN)", true); + + Statement update = CCJSqlParserUtil.parse("UPDATE t SET a = 1 OPTION (RECOMPILE)"); + Assertions.assertNotNull(((net.sf.jsqlparser.statement.update.Update) update).getOption()); + } + + @Test + public void testOptionOptimizeForUnknownParameter() throws JSQLParserException { + String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p UNKNOWN))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals(1, optimizeFor.getParameters().size()); + Column parameter = + Assertions.assertInstanceOf(Column.class, optimizeFor.getParameters().get(0)); + Assertions.assertEquals("@p UNKNOWN", parameter.getColumnName()); + } + + @Test + public void testOptionOptimizeForMixedParameters() throws JSQLParserException { + String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p = 1, @q UNKNOWN))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals(2, optimizeFor.getParameters().size()); + Assertions.assertEquals("@p = 1", optimizeFor.getParameters().get(0).toString()); + Column parameter = + Assertions.assertInstanceOf(Column.class, optimizeFor.getParameters().get(1)); + Assertions.assertEquals("@q UNKNOWN", parameter.getColumnName()); + } + + @Test + public void testOptionAfterInsertValuesIsRejected() { + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse( + "INSERT INTO t (a) VALUES (1) OPTION (RECOMPILE)")); + } + + @Test + public void testOptionAfterInsertSelect() throws JSQLParserException { + String sql = "INSERT INTO t (a) SELECT a FROM s OPTION (RECOMPILE)"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + Insert insert = (Insert) statement; + Assertions.assertNotNull(insert.getSelect().getOption()); + } + + @Test + public void testOptionAfterMergeStatement() throws JSQLParserException { + String sql = + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = s.a OPTION (HASH JOIN)"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + Merge merge = (Merge) statement; + Assertions.assertNotNull(merge.getOption()); + Assertions.assertEquals("HASH JOIN", merge.getOption().getOptionHints().get(0).getName()); + } + + @Test + public void testOptionAfterMergeOutputClause() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = s.a " + + "OUTPUT deleted.a OPTION (HASH JOIN)", + false); + } + + @Test + public void testOptionAsIdentifierStillWorks() throws JSQLParserException { + // OPTION stays a non-reserved keyword usable as column and table name + assertSqlCanBeParsedAndDeparsed("SELECT option FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM option", true); + assertSqlCanBeParsedAndDeparsed( + "SELECT a, option AS o FROM t WHERE option = 1 ORDER BY option", true); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/PivotQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/PivotQueryTest.java new file mode 100644 index 0000000000..37b6a7b270 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/PivotQueryTest.java @@ -0,0 +1,94 @@ +/*- + * #%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.statement.select; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; + +class PivotQueryTest { + + @Test + void testDuckDBPivotStatement() throws JSQLParserException { + String sql = "PIVOT sales ON region USING SUM(amount)"; + + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + PivotQuery pivotQuery = assertInstanceOf(PivotQuery.class, statement); + Table table = assertInstanceOf(Table.class, pivotQuery.getFromItem()); + + assertThat(table.getFullyQualifiedName()).isEqualTo("sales"); + assertThat(pivotQuery.getOnExpressions()).hasSize(1); + assertThat(pivotQuery.getUsingItems()).hasSize(1); + assertThat(TablesNamesFinder.findTables(sql)).containsExactly("sales"); + } + + @Test + void testDuckDBPivotInSubquery() throws JSQLParserException { + String sql = "SELECT * FROM (PIVOT sales ON region USING SUM(amount))"; + + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + ParenthesedSelect parenthesedSelect = + assertInstanceOf(ParenthesedSelect.class, select.getPlainSelect().getFromItem()); + + assertInstanceOf(PivotQuery.class, parenthesedSelect.getSelect()); + assertThat(TablesNamesFinder.findTables(sql)).containsExactly("sales"); + } + + @Test + void testDuckDBPivotFullSimplifiedSyntax() throws JSQLParserException { + String sql = "PIVOT sales ON region, category " + + "USING SUM(amount) AS total, COUNT(*) AS count " + + "GROUP BY year ORDER BY year DESC LIMIT 10"; + + PivotQuery pivotQuery = (PivotQuery) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + + assertThat(pivotQuery.getOnExpressions()).hasSize(2); + assertThat(pivotQuery.getUsingItems()).hasSize(2); + assertThat(pivotQuery.getGroupByExpressions()).hasSize(1); + assertThat(pivotQuery.getOrderByElements()).hasSize(1); + assertNotNull(pivotQuery.getLimit()); + } + + @Test + void testDuckDBPivotOptionalClauses() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("PIVOT sales ON region", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("PIVOT sales USING SUM(amount)", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("PIVOT sales GROUP BY region", true); + } + + @Test + void testDuckDBPivotInCreateView() throws JSQLParserException { + String sql = "CREATE VIEW v AS PIVOT sales ON region USING SUM(amount)"; + + CreateView createView = + (CreateView) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + + assertInstanceOf(PivotQuery.class, createView.getSelect()); + } + + @Test + void testExistingPivotClauseStillWorks() throws JSQLParserException { + String sql = "SELECT * FROM sales " + + "PIVOT (SUM(amount) FOR region IN ('US', 'EU'))"; + + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + Table table = assertInstanceOf(Table.class, select.getPlainSelect().getFromItem()); + + assertNotNull(table.getPivot()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java b/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java index 9ded167869..fc97c2f4a4 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java @@ -13,6 +13,8 @@ import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.Intersects; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; @@ -107,11 +109,10 @@ void testNextValueIssue1863() throws JSQLParserException { } @Test - @Disabled - // wip void testDollarQuotedText() throws JSQLParserException { String sqlStr = "SELECT $tag$This\nis\na\nselect\ntest\n$tag$ from dual where a=b"; - PlainSelect st = (PlainSelect) CCJSqlParserUtil.parse(sqlStr); + PlainSelect st = (PlainSelect) CCJSqlParserUtil.parse(sqlStr, + parser -> parser.withDialect(Dialect.POSTGRESQL)); StringValue stringValue = st.getSelectItem(0).getExpression(StringValue.class); @@ -138,4 +139,44 @@ void testQuotedIdentifier() throws JSQLParserException { Assertions.assertEquals("`This is a Test Table`", table.getName()); } + + @Test + public void testPostgresHashBinaryOperator() throws JSQLParserException { + // PostgreSQL 18, Table 9.4 (Mathematical Operators): + // integral_type # integral_type -> bitwise exclusive OR + assertSqlCanBeParsedAndDeparsed("SELECT 17 # 5"); + // PostgreSQL 18, Table 9.36 (Geometric Operators): + // geometric_type # geometric_type -> point of intersection + Select select = (Select) assertSqlCanBeParsedAndDeparsed("SELECT lseg1 # lseg2"); + Intersects intersects = Assertions.assertInstanceOf(Intersects.class, + select.getPlainSelect().getSelectItem(0).getExpression()); + Assertions.assertEquals("#", intersects.getStringExpression()); + Assertions.assertInstanceOf(Column.class, intersects.getLeftExpression()); + Assertions.assertInstanceOf(Column.class, intersects.getRightExpression()); + + assertSqlCanBeParsedAndDeparsed("SELECT a # (b + 1) FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT a # b AS x FROM t"); + // same tier as `&`: binds tighter than the comparison that follows + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a # b = 1"); + } + + @Test + public void testPostgresHashOperatorKeepsIdentifiersAndJsonOperators() + throws JSQLParserException { + // `#` stays an identifier character: SQL Server `#temp`, `##global` + // (#1197), Oracle `#$tab1#` and unquoted names containing `#` + assertSqlCanBeParsedAndDeparsed("SELECT #temp FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT ##global FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT #$tab1# FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT a#b FROM t"); + // `#>` / `#>>` keep their JSON lexing (#1695) + assertSqlCanBeParsedAndDeparsed("SELECT data #> '{a,b}' FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT data #>> '{0,1}' FROM t"); + // a lone `#` can no longer be an identifier (alias, column or table + // name), all such forms fail loudly now that `#` is an operator + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT 1 #")); + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT # FROM t")); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/RowsFromColumnDefinitionTest.java b/src/test/java/net/sf/jsqlparser/statement/select/RowsFromColumnDefinitionTest.java new file mode 100644 index 0000000000..46065348d7 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/RowsFromColumnDefinitionTest.java @@ -0,0 +1,129 @@ +/*- + * #%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.statement.select; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; + +class RowsFromColumnDefinitionTest { + + // Accepted/rejected cases are data records, not executable SQL batch scripts. + // Both outcomes were checked against PostgreSQL 18.6. + @ParameterizedTest + @CsvFileSource(resources = "rows-from-cases.tsv", delimiter = '\t') + void preservesValidDefinitionsAndRejectsMalformedLists(boolean valid, String sql) + throws JSQLParserException { + if (!valid) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + return; + } + PlainSelect select = parse(sql); + StringBuilder visitor = new StringBuilder(); + select.accept(new StatementDeParser(visitor)); + for (String rendered : List.of(select.toString(), visitor.toString())) { + PlainSelect reparsed = parse(rendered); + assertEquals(select.toString(), reparsed.toString()); + assertEquals(select.getFromItem().getClass(), reparsed.getFromItem().getClass()); + } + } + + @Test + void eachFunctionOwnsItsDefinitionsIndependentlyOfTheOuterAlias() + throws JSQLParserException { + PlainSelect select = parse("SELECT * FROM ROWS FROM (" + + "json_to_record('{}') AS (amount numeric(10, 2), label text COLLATE \"C\"), " + + "generate_series(1, 2), json_to_record('{}') AS (items integer[])) " + + "WITH ORDINALITY AS r(total, name, n, data, ord)"); + TableFunction table = (TableFunction) select.getFromItem(); + assertTrue(table.isRowsFrom()); + assertEquals(3, table.getRowsFromFunctions().size()); + assertEquals("ORDINALITY", table.getWithClause()); + assertEquals(5, table.getAlias().getAliasColumns().size()); + List first = table.getFunctions().get(0).getResultColumnDefinitions(); + assertEquals("amount", first.get(0).getColumnName()); + assertEquals(10, first.get(0).getColDataType().getPrecision()); + assertEquals(2, first.get(0).getColDataType().getScale()); + assertEquals("\"C\"", first.get(1).getColumnOptions().get(0).getCollation()); + assertNull(table.getFunctions().get(1).getResultColumnDefinitions()); + assertEquals(1, table.getFunctions().get(2).getResultColumnDefinitions().get(0) + .getColDataType().getArrayData().size()); + + first.get(0).setColumnName("changed"); + first.get(0).setColDataType(new ColDataType("bigint")); + TableFunction reparsed = (TableFunction) parse(select.toString()).getFromItem(); + assertEquals("changed", reparsed.getFunctions().get(0).getResultColumnDefinitions() + .get(0).getColumnName()); + assertEquals("bigint", reparsed.getFunctions().get(0).getResultColumnDefinitions() + .get(0).getColDataType().getDataType()); + } + + @Test + void legacyFunctionListEditsKeepDefinitionsAttachedToTheirFunction() + throws JSQLParserException { + TableFunction table = (TableFunction) parse("SELECT * FROM ROWS FROM (" + + "json_to_record('{}') AS (a integer), generate_series(1, 2))").getFromItem(); + Function record = table.getRowsFromFunctions().remove(0); + table.getRowsFromFunctions().add(record); + TableFunction reparsed = (TableFunction) parse("SELECT * FROM " + table).getFromItem(); + assertNull(reparsed.getFunctions().get(0).getResultColumnDefinitions()); + assertNotNull(reparsed.getFunctions().get(1).getResultColumnDefinitions()); + } + + @Test + void tableFunctionRendererVisitsFunctionArguments() throws JSQLParserException { + PlainSelect select = parse("SELECT * FROM ROWS FROM (json_to_record('{}') AS (a integer), " + + "generate_series(1, 2)) WITH ORDINALITY AS r(a, n, ord)"); + String original = select.toString(); + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 10); + } + }; + select.accept(new StatementDeParser(expressions, new SelectDeParser(), sql)); + assertTrue(sql.toString().contains("generate_series(11, 12)")); + assertTrue(sql.toString().contains("AS (a integer)")); + assertEquals(original, select.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } + + @Test + void tableDiscoveryStillVisitsSubqueriesInRecordFunctionArguments() + throws JSQLParserException { + String sql = "SELECT * FROM ROWS FROM (" + + "json_to_record((SELECT payload FROM events)) AS (a integer))"; + assertEquals(Set.of("events"), TablesNamesFinder.findTables(sql)); + } + + private static PlainSelect parse(String sql) throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java index ecb1204956..6ef751ac10 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java @@ -10,8 +10,16 @@ package net.sf.jsqlparser.statement.select; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertThrows; public class SQLiteTest { @Test @@ -19,4 +27,29 @@ void testInsertOrReplaceUpsert() throws JSQLParserException { String sqlString = "INSERT OR REPLACE INTO kjobLocks VALUES (?, ?, ?)"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true); } + + @ParameterizedTest + @ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"}) + void testInsertOrConflictAlgorithm(String algorithm) throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "INSERT OR " + algorithm + " INTO t (a, b) VALUES (1, 2)", true, + parser -> parser.withDialect(Dialect.SQLITE)); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "insert or " + algorithm.toLowerCase(Locale.ROOT) + " into t select * from s", true, + parser -> parser.withDialect(Dialect.SQLITE)); + } + + @ParameterizedTest + @ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "ROLLBACK"}) + void testInsertOrConflictAlgorithmNeedsSqliteDialect(String algorithm) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("INSERT OR " + algorithm + " INTO t VALUES (1)")); + } + + @Test + void testInsertOrUnknownAlgorithm() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("INSERT OR NOTHING INTO t VALUES (1)", + parser -> parser.withDialect(Dialect.SQLITE))); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java index ddc578d269..f46c1a2384 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java @@ -12,8 +12,13 @@ import java.util.ArrayList; import java.util.List; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.ASTNodeAccess; import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor; import net.sf.jsqlparser.parser.CCJSqlParserTreeConstants; +import net.sf.jsqlparser.parser.CCJSqlParser; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.parser.Node; import net.sf.jsqlparser.parser.Token; @@ -175,10 +180,51 @@ public Object visit(Node node, Object data) { assertNotNull(subSelectStart); assertNotNull(subSelectEnd); - assertEquals(32, subSelectStart.beginColumn); + // the node spans the whole IN expression, so it starts at the left operand + assertEquals(30, subSelectStart.beginColumn); assertEquals(49, subSelectEnd.endColumn); } + @Test + public void testBinaryConditionNodeStartsAtItsLeftOperand() throws JSQLParserException { + String[][] cases = { + {"SELECT * FROM t WHERE z = 0 AND a = 1", "a = 1"}, + {"SELECT * FROM t WHERE z = 0 AND a IN (1, 2)", "a IN (1, 2)"}, + {"SELECT * FROM t WHERE z = 0 AND a LIKE 'p'", "a LIKE 'p'"}, + {"SELECT * FROM t WHERE z = 0 AND a SIMILAR TO 'p'", "a SIMILAR TO 'p'"}, + {"SELECT * FROM t WHERE z = 0 AND a IS DISTINCT FROM 1", "a IS DISTINCT FROM 1"}}; + + for (String[] testCase : cases) { + String sql = testCase[0]; + Expression condition = ((AndExpression) ((PlainSelect) CCJSqlParserUtil.parse(sql)) + .getWhere()).getRightExpression(); + Node node = ((ASTNodeAccess) condition).getASTNode(); + assertEquals(testCase[1], sql.substring(node.jjtGetFirstToken().absoluteBegin - 1, + node.jjtGetLastToken().absoluteEnd - 1), sql); + } + } + + @Test + public void testOperatorExpressionNodeSpansItsOperands() throws JSQLParserException { + String sql = "SELECT a - 1 - 2, 1 + 2 * 3, a | | b FROM t WHERE d > CURRENT_DATE - 7"; + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql); + BinaryExpression chain = (BinaryExpression) select.getSelectItem(0).getExpression(); + BinaryExpression precedence = (BinaryExpression) select.getSelectItem(1).getExpression(); + + assertEquals("a - 1 - 2", source(sql, chain)); + assertEquals("a - 1", source(sql, chain.getLeftExpression())); + assertEquals("2 * 3", source(sql, precedence.getRightExpression())); + assertEquals("a | | b", source(sql, select.getSelectItem(2).getExpression())); + assertEquals("CURRENT_DATE - 7", + source(sql, ((BinaryExpression) select.getWhere()).getRightExpression())); + } + + private static String source(String sql, Expression expression) { + Node node = expression.getASTNode(); + return sql.substring(node.jjtGetFirstToken().absoluteBegin - 1, + node.jjtGetLastToken().absoluteEnd - 1); + } + @Test public void testSelectASTExtractWithCommentsIssue1580() throws JSQLParserException { String sql = @@ -212,4 +258,21 @@ public void testSelectASTExtractWithCommentsIssue1580_2() throws JSQLParserExcep assertThat(root.jjtGetFirstToken().specialToken.image) .isEqualTo("/* I want this comment */"); } + + @Test + public void testSelectASTHashLineCommentImage() throws Exception { + // a `#` comment under allowHashLineComments is a normal special + // token carrying the original `# ...` text + CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT 1 # note\nFROM t"); + parser.withHashLineComments(true); + parser.Statement(); + List comments = new ArrayList<>(); + for (Token t = parser.getASTRoot().jjtGetFirstToken(); t.next != null; t = t.next) { + for (Token sp = t.specialToken; sp != null; sp = sp.specialToken) { + comments.add(sp); + } + } + + assertThat(comments).extracting(token -> token.image).containsExactly("# note"); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index d0000c1389..122b13ba45 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -36,12 +36,14 @@ import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.AllValue; import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.CollateExpression; import net.sf.jsqlparser.expression.DoubleValue; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Function; import net.sf.jsqlparser.expression.IntervalExpression; import net.sf.jsqlparser.expression.JdbcNamedParameter; import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.NotExpression; import net.sf.jsqlparser.expression.NullValue; @@ -59,6 +61,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThan; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.LikeExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserManager; import net.sf.jsqlparser.parser.CCJSqlParserUtil; @@ -88,6 +91,8 @@ @Execution(ExecutionMode.CONCURRENT) public class SelectTest { + private static final String LONG_CONDITION = "B = 1 AND ".repeat(150) + "B = 1"; + private final CCJSqlParserManager parserManager = new CCJSqlParserManager(); @Test @@ -1001,7 +1006,8 @@ public void testUnion() throws JSQLParserException { assertEquals("mytable2", ((Table) ((PlainSelect) setList.getSelects().get(2)).getFromItem()).getName()); assertEquals(3, - ((LongValue) setList.getSelects().get(2).getLimit().getOffset()).getValue()); + ((LongValue) setList.getLimit().getOffset()).getValue()); + assertNull(setList.getSelects().get(2).getLimit()); // with fetch and with ur @@ -1039,11 +1045,13 @@ public void testUnion2() throws JSQLParserException { assertEquals("mytable2", ((Table) ((PlainSelect) setList.getSelects().get(2)).getFromItem()).getName()); assertEquals(3, - ((LongValue) setList.getSelects().get(2).getLimit().getRowCount()) + ((LongValue) setList.getLimit().getRowCount()) .getValue()); - assertNull(setList.getSelects().get(2).getLimit().getOffset()); + assertNull(setList.getLimit().getOffset()); assertEquals(new LongValue(4), - setList.getSelects().get(2).getOffset().getOffset()); + setList.getOffset().getOffset()); + assertNull(setList.getSelects().get(2).getLimit()); + assertNull(setList.getSelects().get(2).getOffset()); } @@ -1374,6 +1382,47 @@ public void testGroupBy() throws JSQLParserException { assertStatementCanBeDeparsedAs(select, statement); } + @Test + public void testIssue2485OracleParenthesizedFullyQualifiedGroupByColumn() + throws JSQLParserException { + String sql = "SELECT sys.dual.dummy, count(*)\n" + + "FROM sys.dual\n" + + "GROUP BY (sys.dual.dummy), (sys.dual.dummy)"; + + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + Select select = assertInstanceOf(Select.class, statement); + PlainSelect plainSelect = select.getPlainSelect(); + assertNotNull(plainSelect.getGroupBy()); + + ExpressionList groupByExpressions = plainSelect.getGroupBy() + .getGroupByExpressionList(); + assertEquals(2, groupByExpressions.size()); + for (Expression groupByExpression : groupByExpressions) { + ParenthesedExpressionList parenthesedExpression = assertInstanceOf( + ParenthesedExpressionList.class, groupByExpression); + Column column = assertInstanceOf(Column.class, parenthesedExpression.get(0)); + assertEquals("sys.dual.dummy", column.getFullyQualifiedName()); + } + } + + @Test + public void testIssue2485OracleParenthesizedFourPartGroupByColumn() + throws JSQLParserException { + String sql = "SELECT 1\n" + + "FROM sys.dual\n" + + "GROUP BY sys.dual.dummy, (catalog.sys.dual.dummy)"; + + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + Select select = assertInstanceOf(Select.class, statement); + ExpressionList groupByExpressions = select.getPlainSelect().getGroupBy() + .getGroupByExpressionList(); + assertEquals(2, groupByExpressions.size()); + ParenthesedExpressionList parenthesedExpression = assertInstanceOf( + ParenthesedExpressionList.class, groupByExpressions.get(1)); + Column column = assertInstanceOf(Column.class, parenthesedExpression.get(0)); + assertEquals("catalog.sys.dual.dummy", column.getFullyQualifiedName()); + } + @Test public void testHaving() throws JSQLParserException { String statement = @@ -1907,6 +1956,29 @@ public void testSelectFunction() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(statement); } + @Test + public void testSelectWithoutFromRetainsWhereGroupByHaving() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT 1 WHERE 1 = 1"); + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT 1 GROUP BY 1 HAVING 1 = 1"); + assertNotNull(plainSelect.getGroupBy()); + assertNotNull(plainSelect.getHaving()); + } + + @Test + public void testSelectWithoutFromRetainsWindowAndQualify() throws JSQLParserException { + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT 1 WINDOW w AS (ORDER BY 1)"); + assertEquals(1, plainSelect.getWindowDefinitions().size()); + assertSqlCanBeParsedAndDeparsed("SELECT 1 QUALIFY 1 = 1"); + } + + @Test + public void testSelectWithoutFromRetainsHierarchicalAndPreferring() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT 1 START WITH 1 = 1 CONNECT BY LEVEL <= 1"); + assertSqlCanBeParsedAndDeparsed("SELECT 1 PREFERRING HIGH 1"); + } + @Test public void testWeirdSelect() throws JSQLParserException { String sql = @@ -2316,6 +2388,45 @@ public void testIsNotFalse() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(statement); } + @Test + public void testBooleanTestAfterPredicate() throws JSQLParserException { + // SQL:2016 6.39: a boolean test applies to any boolean primary, so an + // IS TRUE/FALSE test chains onto a comparison or predicate result + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a > 0 IS TRUE"); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a IN (1, 2) IS FALSE"); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a IS NULL IS NOT TRUE"); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a IS TRUE IS FALSE"); + } + + @Test + public void testSelectWithoutTargetListPostgreSql() throws JSQLParserException { + // PostgreSQL permits SELECT with no target expressions + Statement stmt = CCJSqlParserUtil.parse( + "SELECT FROM t", parser -> parser.withDialect(Dialect.POSTGRESQL)); + PlainSelect select = (PlainSelect) ((Select) stmt).getSelectBody(); + assertTrue(select.getSelectItems().isEmpty()); + assertDeparse(select, "SELECT FROM t"); + + // the reported case: empty target list inside a derived table + assertSqlCanBeParsedAndDeparsed( + "SELECT count(*) FROM (SELECT FROM (VALUES (1, 'a'), (2, 'b')) AS t(id, label)) AS q", + false, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + @Test + public void testGroupByAllDistinctPostgreSql() throws JSQLParserException { + // PostgreSQL: GROUP BY [ ALL | DISTINCT ] grouping_element [, ...] + assertSqlCanBeParsedAndDeparsed( + "SELECT 1 GROUP BY DISTINCT GROUPING SETS ((1), (1))", + false, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertSqlCanBeParsedAndDeparsed( + "SELECT 1 GROUP BY ALL GROUPING SETS ((1), (2))", + false, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertSqlCanBeParsedAndDeparsed( + "SELECT a FROM t GROUP BY DISTINCT a", + false, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + @Test public void testIsUnknown() throws JSQLParserException { String statement = "SELECT col FROM tbl WHERE col IS UNKNOWN"; @@ -3181,6 +3292,13 @@ public void testJsonExpression() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed( "SELECT json_typeof(sale->'items'), json_typeof(sale->'items'->'quantity') FROM sales"); + Select jsonSelect = (Select) CCJSqlParserUtil.parse("SELECT data->'images' FROM instagram"); + assertInstanceOf(JsonExpression.class, + jsonSelect.getPlainSelect().getSelectItem(0).getExpression()); + jsonSelect = (Select) CCJSqlParserUtil.parse("SELECT data->>'images' FROM instagram"); + assertInstanceOf(JsonExpression.class, + jsonSelect.getPlainSelect().getSelectItem(0).getExpression()); + // The following staments can be parsed but not deparsed for (String statement : new String[] { "SELECT doc->'site_name' FROM websites WHERE doc @> '{\"tags\":[{\"term\":\"paris\"}, {\"term\":\"food\"}]}'", @@ -4160,6 +4278,38 @@ public void testEscaped() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT _utf8'testvalue'"); } + @ParameterizedTest + @ValueSource(strings = {"_utf8mb4'testvalue'", "_utf8mb4 'testvalue'", + "_utf8 'testvalue'", "_latin1'testvalue'", "_binary'testvalue'", + "_ISO8859_1 'testvalue'"}) + public void testCharacterSetIntroducedString(String expression) throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT " + expression); + SelectItem selectItem = select.getPlainSelect().getSelectItem(0); + StringValue stringValue = assertInstanceOf(StringValue.class, selectItem.getExpression()); + + assertEquals(expression.substring(0, expression.indexOf('\'')).trim(), + stringValue.getPrefix()); + assertEquals("testvalue", stringValue.getValue()); + assertNull(selectItem.getAlias()); + } + + @Test + public void testCharacterSetIntroducedStringWithCollate() throws JSQLParserException { + String sql = "SELECT _utf8mb4'some text' COLLATE utf8mb4_unicode_ci AS custom_string"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + SelectItem selectItem = select.getPlainSelect().getSelectItem(0); + CollateExpression collateExpression = + assertInstanceOf(CollateExpression.class, selectItem.getExpression()); + StringValue stringValue = assertInstanceOf(StringValue.class, + collateExpression.getLeftExpression()); + + assertEquals("_utf8mb4", stringValue.getPrefix()); + assertEquals("some text", stringValue.getValue()); + assertEquals("utf8mb4_unicode_ci", collateExpression.getCollate()); + assertEquals("custom_string", selectItem.getAlias().getName()); + assertEquals(sql, select.toString()); + } + @Test public void testIssue563MultiSubJoin() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed( @@ -5253,6 +5403,18 @@ public void testSetOperationWithParenthesisIssue1094() throws JSQLParserExceptio "SELECT * FROM ((SELECT A FROM tbl) UNION DISTINCT (SELECT B FROM tbl2)) AS union1"); } + @Test + public void testSetOperationWithParenthesisAndLongFirstBranch() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT * FROM ((SELECT A FROM tbl WHERE " + LONG_CONDITION + + ") UNION ALL (SELECT A FROM tbl2)) AS union1"); + } + + @Test + public void testSetOperationInExpressionWithLongFirstBranch() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT * FROM tbl WHERE A IN ((SELECT A FROM tbl2 WHERE " + + LONG_CONDITION + ") UNION ALL (SELECT A FROM tbl3))"); + } + @Test public void testSetOperationWithParenthesisIssue1094_2() throws JSQLParserException { String sqlStr = @@ -6527,4 +6689,33 @@ void testIssue2445FunctionWithBracketParameters() throws JSQLParserException { " , cast(ex.value_date - f.appraisal_date AS DECIMAL) / 365 )"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testClickHouseArrayJoin() throws Exception { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, x FROM t LEFT ARRAY JOIN arr AS x", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, x FROM t ARRAY JOIN arr AS x", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, arr FROM t ARRAY JOIN arr", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, arr FROM t LEFT ARRAY JOIN arr", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT s, x, o.name FROM t LEFT ARRAY JOIN arr AS x INNER JOIN other_table o ON t.id = o.t_id", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH exploded AS (SELECT s, x FROM t LEFT ARRAY JOIN arr AS x) SELECT * FROM exploded", + true); + + PlainSelect selectLeft = + (PlainSelect) CCJSqlParserUtil.parse("SELECT s, x FROM t LEFT ARRAY JOIN arr AS x"); + Join joinLeft = selectLeft.getJoins().get(0); + Assertions.assertTrue(joinLeft.isArray(), "Should be an array join"); + Assertions.assertTrue(joinLeft.isLeft(), "Should be a left join"); + + PlainSelect selectInner = + (PlainSelect) CCJSqlParserUtil.parse("SELECT s, x FROM t ARRAY JOIN arr AS x"); + Join joinInner = selectInner.getJoins().get(0); + Assertions.assertTrue(joinInner.isArray(), "Should be an array join"); + Assertions.assertFalse(joinInner.isLeft(), "Should not be a left join"); + } + } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java new file mode 100644 index 0000000000..9bb5352558 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java @@ -0,0 +1,65 @@ +/*- + * #%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.statement.select; + +import net.sf.jsqlparser.JSQLParserException; +import org.junit.jupiter.api.Test; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +/** + * XML constructor functions taking a bare leading keyword before their first argument, e.g. + * {@code XMLPARSE(CONTENT expr)}. + */ +public class SelectXMLParseTest { + + @Test + public void testXmlParseContent() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(content a) FROM mytable"); + } + + @Test + public void testXmlParseDocument() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(document a) FROM mytable"); + } + + @Test + public void testXmlParseUpperCaseKeyword() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(CONTENT a) FROM mytable"); + } + + @Test + public void testXmlAggWithXmlParse() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "SELECT xmlagg(xmlparse(content sn.CODE || ',') ORDER BY sn.CODE).getclobval() AS SN FROM GV_SYS_CODEINFO sn"); + } + + @Test + public void testXmlElementName() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlelement(name a, b) FROM mytable"); + } + + @Test + public void testXmlForestName() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlforest(name a) FROM mytable"); + } + + /** + * CONTENT, DOCUMENT and NAME must stay plain identifiers everywhere else. + */ + @Test + public void testKeywordsRemainIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT content, document, name FROM mytable"); + assertSqlCanBeParsedAndDeparsed("SELECT content FROM content c WHERE c.document = 1"); + assertSqlCanBeParsedAndDeparsed("SELECT t.name AS document FROM mytable t"); + assertSqlCanBeParsedAndDeparsed("SELECT f(content) FROM mytable"); + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(content) FROM mytable"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SetOperationLimitTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SetOperationLimitTest.java new file mode 100644 index 0000000000..28bc2a8cde --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/SetOperationLimitTest.java @@ -0,0 +1,201 @@ +/*- + * #%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.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SetOperationLimitTest { + + @ParameterizedTest + @MethodSource("globalLimitCases") + void trailingLimitBelongsToSetOperation(String sql, String rowCount, String limitOffset, + String offset) throws JSQLParserException { + SetOperationList select = (SetOperationList) assertSqlCanBeParsedAndDeparsed(sql); + assertNotNull(select.getLimit(), sql); + assertEquals(rowCount, select.getLimit().getRowCount().toString()); + assertEquals(limitOffset, select.getLimit().getOffset() == null ? null + : select.getLimit().getOffset().toString()); + assertEquals(offset, select.getOffset() == null ? null + : select.getOffset().getOffset().toString()); + Select last = select.getSelects().get(select.getSelects().size() - 1); + assertNull(last.getLimit()); + assertNull(last.getOffset()); + if (sql.contains("ORDER BY")) { + assertNotNull(select.getOrderByElements()); + assertEquals(1, select.getOrderByElements().size()); + assertNull(last.getOrderByElements()); + } + } + + static Stream globalLimitCases() { + return Stream.of( + Arguments.of("SELECT a FROM t1 UNION SELECT b FROM t2 LIMIT 1", "1", null, null), + Arguments.of("SELECT a FROM t1 UNION ALL SELECT b FROM t2 LIMIT 1", "1", null, + null), + Arguments.of("SELECT a FROM t1 INTERSECT SELECT b FROM t2 LIMIT 1", "1", null, + null), + Arguments.of("SELECT a FROM t1 EXCEPT SELECT b FROM t2 LIMIT 1", "1", null, null), + Arguments.of("SELECT a FROM t1 MINUS SELECT b FROM t2 LIMIT 1", "1", null, null), + Arguments.of("SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 LIMIT 1", "1", null, + null), + Arguments.of("WITH t AS (SELECT 1 AS a) SELECT a FROM t UNION SELECT 2 LIMIT 1", + "1", null, null), + Arguments.of("SELECT a FROM t1 UNION (SELECT b FROM t2) LIMIT 1", "1", null, null), + Arguments.of("SELECT a FROM t1 UNION SELECT b FROM t2 ORDER BY b LIMIT 1", "1", + null, null), + Arguments.of( + "SELECT id FROM table1 UNION SELECT id FROM table2 ORDER BY id ASC LIMIT 55", + "55", null, null), + Arguments.of( + "SELECT * FROM table1 UNION SELECT * FROM table2 ORDER BY col LIMIT 4 OFFSET 5", + "4", null, "5"), + Arguments.of("SELECT a FROM t1 UNION SELECT b FROM t2 LIMIT (1 + 2)", "(1 + 2)", + null, null), + Arguments.of("SELECT a FROM t1 UNION SELECT b FROM t2 LIMIT ? OFFSET ?", "?", null, + "?"), + Arguments.of("SELECT a FROM t1 UNION SELECT b FROM t2 LIMIT :cap OFFSET :skip", + ":cap", null, ":skip"), + Arguments.of("SELECT a FROM t1 UNION SELECT b FROM t2 LIMIT ALL OFFSET 2", "ALL", + null, "2"), + Arguments.of("SELECT a FROM t1 UNION SELECT b FROM t2 LIMIT NULL", "NULL", null, + null), + Arguments.of("SELECT a FROM t1 UNION SELECT b FROM t2 LIMIT 2, 3", "3", "2", null), + Arguments.of("SELECT a FROM t1 UNION SELECT b FROM t2 LIMIT 3 OFFSET 2", "3", null, + "2")); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT a FROM t1 UNION SELECT b FROM t2 OFFSET 2", + "SELECT a FROM t1 UNION SELECT b FROM t2 OFFSET 2 ROWS FETCH NEXT 1 ROWS ONLY", + "SELECT a FROM t1 UNION SELECT b FROM t2 ORDER BY b OFFSET 2 ROWS FETCH NEXT 1 ROWS ONLY" + }) + void trailingOffsetBelongsToSetOperation(String sql) throws JSQLParserException { + SetOperationList select = (SetOperationList) assertSqlCanBeParsedAndDeparsed(sql); + assertNotNull(select.getOffset()); + assertEquals(new LongValue(2), select.getOffset().getOffset()); + assertNull(select.getSelects().get(1).getOffset()); + if (sql.contains("FETCH")) { + assertEquals(1, select.getFetch().getRowCount()); + assertNull(select.getSelects().get(1).getFetch()); + } + } + + @Test + void fetchWithoutOffsetStillBelongsToSetOperation() throws JSQLParserException { + SetOperationList select = (SetOperationList) assertSqlCanBeParsedAndDeparsed( + "SELECT a FROM t1 UNION SELECT b FROM t2 FETCH FIRST 1 ROWS ONLY"); + assertEquals(1, select.getFetch().getRowCount()); + assertNull(select.getSelects().get(1).getFetch()); + } + + @Test + void plainSelectKeepsItsLimitAndOffset() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT a FROM t LIMIT 1 OFFSET 2"); + assertEquals(new LongValue(1), select.getLimit().getRowCount()); + assertEquals(new LongValue(2), select.getOffset().getOffset()); + } + + @ParameterizedTest + @ValueSource(strings = {"LIMIT 1", "LIMIT 1 OFFSET 2", "ORDER BY b LIMIT 1 OFFSET 2", + "OFFSET 2 ROWS FETCH FIRST 1 ROWS ONLY"}) + void parenthesizedBranchKeepsItsOwnClauses(String clauses) throws JSQLParserException { + SetOperationList select = (SetOperationList) assertSqlCanBeParsedAndDeparsed( + "SELECT a FROM t1 UNION (SELECT b FROM t2 " + clauses + ")"); + assertNull(select.getLimit()); + assertNull(select.getOffset()); + assertNull(select.getFetch()); + assertNull(select.getOrderByElements()); + PlainSelect branch = ((ParenthesedSelect) select.getSelects().get(1)).getPlainSelect(); + assertEquals("SELECT b FROM t2 " + clauses, branch.toString()); + if (clauses.contains("LIMIT")) { + assertEquals(new LongValue(1), branch.getLimit().getRowCount()); + } + if (clauses.contains("OFFSET")) { + assertEquals(new LongValue(2), branch.getOffset().getOffset()); + } + if (clauses.contains("FETCH")) { + assertEquals(1, branch.getFetch().getRowCount()); + } + } + + @Test + void branchAndSetOperationCanBothHaveLimits() throws JSQLParserException { + SetOperationList select = (SetOperationList) assertSqlCanBeParsedAndDeparsed( + "(SELECT 1 LIMIT 1) UNION ALL (SELECT 2 LIMIT 2 OFFSET 1) LIMIT 3 OFFSET 4"); + assertEquals(new LongValue(3), select.getLimit().getRowCount()); + assertEquals(new LongValue(4), select.getOffset().getOffset()); + assertEquals(new LongValue(1), + ((ParenthesedSelect) select.getSelects().get(0)).getSelect().getLimit() + .getRowCount()); + Select last = ((ParenthesedSelect) select.getSelects().get(1)).getSelect(); + assertEquals(new LongValue(2), last.getLimit().getRowCount()); + assertEquals(new LongValue(1), last.getOffset().getOffset()); + } + + @Test + void nestedSetOperationLimitsStayInTheirOwnScope() throws JSQLParserException { + SetOperationList select = (SetOperationList) assertSqlCanBeParsedAndDeparsed( + "SELECT 1 UNION ALL (SELECT 2 UNION ALL SELECT 3 LIMIT 1) LIMIT 2"); + assertEquals(new LongValue(2), select.getLimit().getRowCount()); + SetOperationList inner = + ((ParenthesedSelect) select.getSelects().get(1)).getSetOperationList(); + assertEquals(new LongValue(1), inner.getLimit().getRowCount()); + assertNull(inner.getSelects().get(1).getLimit()); + } + + @Test + void rewritingGlobalLimitChangesWholeResult() throws Exception { + SetOperationList select = (SetOperationList) CCJSqlParserUtil.parse( + "SELECT 1 UNION ALL SELECT 2 LIMIT 1"); + assertEquals(1, rowCount(select.toString())); + assertNotNull(select.getLimit()); + select.getLimit().setRowCount(new LongValue(0)); + assertEquals(0, rowCount(select.toString())); + } + + @Test + void rewritingLocalLimitKeepsOtherBranchRows() throws Exception { + SetOperationList select = (SetOperationList) CCJSqlParserUtil.parse( + "SELECT 1 UNION ALL (SELECT 2 LIMIT 1)"); + assertEquals(2, rowCount(select.toString())); + Select branch = ((ParenthesedSelect) select.getSelects().get(1)).getSelect(); + branch.getLimit().setRowCount(new LongValue(0)); + assertNull(select.getLimit()); + assertEquals(1, rowCount(select.toString())); + } + + private static int rowCount(String sql) throws Exception { + try (Connection connection = DriverManager.getConnection("jdbc:h2:mem:"); + java.sql.Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + int count = 0; + while (resultSet.next()) { + count++; + } + return count; + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SetOperationModifierTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SetOperationModifierTest.java index a30f3e2fa0..5af4513f0a 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SetOperationModifierTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SetOperationModifierTest.java @@ -16,6 +16,7 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.Statement; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.params.ParameterizedTest; @@ -50,6 +51,72 @@ void testSetOperationModifierRoundTrip(String sql) throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sql); } + /** + * The grammar records the modifier verbatim from the source, so the quantifier survives in + * whatever case it was written. Every case spells the same set operation. + * + * @see #2419 + */ + @ParameterizedTest + @ValueSource(strings = {"ALL", "all", "All", "aLL"}) + void testAllModifierRecognisedInAnyCase(String quantifier) throws JSQLParserException { + SetOperation operation = firstOperation( + "SELECT a FROM t1 UNION " + quantifier + " SELECT a FROM t2"); + assertTrue(operation.isAll(), "UNION " + quantifier + " should be an ALL union"); + assertFalse(operation.isDistinct(), "UNION " + quantifier + " is not DISTINCT"); + } + + @ParameterizedTest + @ValueSource(strings = {"DISTINCT", "distinct", "Distinct"}) + void testDistinctModifierRecognisedInAnyCase(String quantifier) throws JSQLParserException { + SetOperation operation = firstOperation( + "SELECT a FROM t1 UNION " + quantifier + " SELECT a FROM t2"); + assertTrue(operation.isDistinct(), "UNION " + quantifier + " should be a DISTINCT union"); + assertFalse(operation.isAll(), "UNION " + quantifier + " is not ALL"); + } + + @ParameterizedTest + @ValueSource(strings = {"EXCEPT", "INTERSECT", "MINUS"}) + void testAllModifierRecognisedInAnyCaseForEverySetOperation(String setOperation) + throws JSQLParserException { + assertTrue(firstOperation("SELECT a FROM t1 " + setOperation + " all SELECT a FROM t2") + .isAll(), setOperation + " all should be an ALL operation"); + } + + /** + * A plain set operation carries no quantifier, and neither predicate may claim one. + */ + @Test + void testUnqualifiedSetOperationIsNeitherAllNorDistinct() throws JSQLParserException { + SetOperation operation = firstOperation("SELECT a FROM t1 UNION SELECT a FROM t2"); + assertFalse(operation.isAll()); + assertFalse(operation.isDistinct()); + } + + /** + * The setters record what they are told rather than the keyword they are named after. + */ + @Test + void testSettersHonourTheirArgument() { + UnionOp union = new UnionOp(); + + union.setAll(true); + assertTrue(union.isAll()); + union.setAll(false); + assertFalse(union.isAll(), "setAll(false) must not leave an ALL modifier behind"); + + union.setDistinct(true); + assertTrue(union.isDistinct()); + union.setDistinct(false); + assertFalse(union.isDistinct(), "setDistinct(false) must not leave a DISTINCT modifier"); + } + + private static SetOperation firstOperation(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertInstanceOf(SetOperationList.class, statement); + return ((SetOperationList) statement).getOperations().get(0); + } + @ParameterizedTest @MethodSource("provideModifierLeakCases") void testModifierDoesNotLeakBetweenOperators(String sql, String forbidden) diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SqlServerXmlMethodTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SqlServerXmlMethodTest.java new file mode 100644 index 0000000000..eabfb60b4c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/SqlServerXmlMethodTest.java @@ -0,0 +1,139 @@ +/*- + * #%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.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.MethodCallExpression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlServerXmlMethodTest { + private static final String SIMPLE = + "SELECT (SELECT body FROM docs FOR XML PATH (''), TYPE).value('.', 'varchar(max)')"; + + private static PlainSelect parse(String sql) throws Exception { + return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.SQLSERVER)); + } + + @Test + void parsesOriginalStuffQueryIssue386() throws Exception { + String sql = "SELECT (STUFF((SELECT '|' + person_name FROM person " + + "JOIN person_group ON person.person_id = person_group.person_id " + + "WHERE person_group.group_id = 1 FOR XML PATH(''), TYPE)" + + ".value('.', 'varchar(max)'), 1, 1, '')) AS person_name"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, + p -> p.withDialect(Dialect.SQLSERVER)); + assertEquals(Set.of("person", "person_group"), + new TablesNamesFinder().getTables(statement)); + assertEquals(statement.toString(), parse(statement.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"value('.', 'varchar(max)')", "query('/root')", "exist('/root')", + "query('/root').value('.', 'int')"}) + void preservesMethodsAndChains(String method) throws Exception { + String sql = "SELECT (SELECT body FROM docs FOR XML PATH(''), TYPE)." + method; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true, + p -> p.withDialect(Dialect.SQLSERVER)); + MethodCallExpression call = (MethodCallExpression) select.getSelectItem(0).getExpression(); + assertInstanceOf(ParenthesedSelect.class, call.getExpression()); + assertEquals(method.substring(0, method.indexOf('(')), call.getMethod().getName()); + assertEquals(select.toString(), parse(select.toString()).toString()); + assertEquals(Set.of("docs"), new TablesNamesFinder().getTables((Statement) select)); + } + + @Test + void visitsReceiverAndMethodArgumentsWithContext() throws Exception { + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }; + SelectVisitorAdapter selects = new SelectVisitorAdapter<>(expressions); + expressions.setSelectVisitor(selects); + parse(SIMPLE).accept(new StatementVisitorAdapter<>(selects), "context"); + assertTrue(values.contains(".")); + assertTrue(values.contains("varchar(max)")); + assertEquals(1, values.stream().filter("."::equals).count()); + } + + @Test + void editsAndDeparsesMethodArguments() throws Exception { + PlainSelect select = parse(SIMPLE); + MethodCallExpression call = (MethodCallExpression) select.getSelectItem(0).getExpression(); + call.setMethod(new Function("query", new StringValue("'/root'"))); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + return getBuilder().append("'changed'"); + } + }; + select.accept(new StatementDeParser(expressions, new SelectDeParser(), output)); + assertTrue(output.toString().endsWith(".query('changed')")); + assertTrue(select.toString().endsWith(".query('/root')")); + assertEquals(output.toString(), parse(output.toString()).toString()); + } + + @Test + void gatesDialectAndRetainsRowNavigation() throws Exception { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(SIMPLE)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.SQLSERVER) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(SIMPLE, p -> p.withDialect(dialect))); + } + } + assertSqlCanBeParsedAndDeparsed("SELECT (row_value).field FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT (row_value).field COLLATE en_US FROM t"); + assertThrows(JSQLParserException.class, + () -> parse(SIMPLE.substring(0, SIMPLE.length() - 1))); + assertEquals(2, CCJSqlParserUtil.parseStatements(SIMPLE + "; SELECT 1;", + p -> p.withDialect(Dialect.SQLSERVER)).size()); + } + + @Test + void validatesMethodAsFunction() { + String sql = "SELECT 1 WHERE " + SIMPLE.substring("SELECT ".length()) + " = 'value'"; + FeatureConfiguration config = new FeatureConfiguration().setValue(Feature.dialect, + Dialect.SQLSERVER.name()); + assertTrue(new Validation(config, List.of(new FeaturesAllowed(Feature.values())), sql) + .validate().isEmpty()); + assertFalse(new Validation(config, + List.of(new FeaturesAllowed(Feature.values()).remove(Feature.function)), sql) + .validate().isEmpty()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/TableStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/select/TableStatementTest.java new file mode 100644 index 0000000000..5fe3cd1e66 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/TableStatementTest.java @@ -0,0 +1,61 @@ +/*- + * #%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.statement.select; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TableStatementTest { + private TableStatement parse(String sql) throws JSQLParserException { + return (TableStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "TABLE inventory.items", + "TABLE `inventory`.`items` ORDER BY id DESC LIMIT 2 OFFSET 1", + "TABLE `db.with.dot`.`table.with.dot` LIMIT 1", + "TABLE items ORDER BY id LIMIT 2" + }) + void preservesTableIdentity(String sql) throws JSQLParserException { + TableStatement statement = parse(sql); + String name = statement.getTable().getFullyQualifiedName(); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + + assertEquals(sql, statement.toString()); + assertEquals(statement.toString(), output.toString()); + assertEquals(name, parse(statement.toString()).getTable().getFullyQualifiedName()); + assertEquals(Set.of(name), new TablesNamesFinder().getTables((Statement) statement)); + } + + @Test + void rendersChangedDatabase() throws JSQLParserException { + TableStatement statement = parse("TABLE old_db.items ORDER BY id LIMIT 1"); + statement.getTable().setSchemaName("new_db"); + String expected = "TABLE new_db.items ORDER BY id LIMIT 1"; + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(expected, statement.toString()); + assertEquals(expected, output.toString()); + assertEquals("new_db", parse(expected).getTable().getSchemaName()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/WithCycleClauseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/WithCycleClauseTest.java new file mode 100644 index 0000000000..f2f6ce86a1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/WithCycleClauseTest.java @@ -0,0 +1,140 @@ +/*- + * #%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.statement.select; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.ValidationError; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class WithCycleClauseTest { + private static final String CTE = "WITH RECURSIVE walk(id, parent) AS " + + "(SELECT id, parent FROM nodes UNION ALL " + + "SELECT n.id, n.parent FROM nodes n JOIN walk w ON n.parent = w.id) "; + + @ParameterizedTest + @ValueSource(strings = { + "CYCLE id SET is_cycle USING path", + "CYCLE id, parent SET is_cycle TO 'Y' DEFAULT 'N' USING path", + "CYCLE id SET is_cycle TO 1 DEFAULT 0 USING path", + "CYCLE id SET is_cycle TO true DEFAULT false USING path", + "SEARCH DEPTH FIRST BY id SET seq CYCLE id SET is_cycle USING path", + "SEARCH BREADTH FIRST BY id, parent SET seq CYCLE id, parent SET is_cycle USING path" + }) + void parsesRecursiveCycleVariants(String clause) throws Exception { + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed( + CTE + clause + " SELECT * FROM walk", true); + assertNotNull(select.getWithItemsList().get(0).getCycleClause()); + assertEquals(select.toString(), CCJSqlParserUtil.parse(select.toString()).toString()); + } + + @Test + void retainsQuotedNamesAndTypedMarkValues() throws Exception { + Select select = (Select) CCJSqlParserUtil.parse(CTE + + "CYCLE id, parent SET \"Cycle\" TO 'yes' DEFAULT 'no' USING \"Path\" SELECT * FROM walk"); + WithCycleClause cycle = select.getWithItemsList().get(0).getCycleClause(); + assertEquals(2, cycle.getCycleColumns().size()); + assertEquals("parent", cycle.getCycleColumns().get(1).getColumnName()); + assertEquals("\"Cycle\"", cycle.getMarkColumnName()); + assertEquals("yes", ((StringValue) cycle.getMarkValue()).getValue()); + assertEquals("no", ((StringValue) cycle.getMarkDefault()).getValue()); + assertEquals("\"Path\"", cycle.getPathColumnName()); + } + + @Test + void preservesImplicitBooleanDefaultsAndMultipleCtes() throws Exception { + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed(CTE + + "CYCLE id SET is_cycle USING path, chosen AS (SELECT * FROM walk) SELECT * FROM chosen", + true); + assertNull(select.getWithItemsList().get(0).getCycleClause().getMarkValue()); + assertNull(select.getWithItemsList().get(0).getCycleClause().getMarkDefault()); + assertNull(select.getWithItemsList().get(1).getCycleClause()); + } + + @ParameterizedTest + @ValueSource(strings = {"CYCLE id SET flag", "CYCLE id SET flag TO 'Y' USING path", + "CYCLE id SET flag DEFAULT 'N' USING path", "CYCLE SET flag USING path"}) + void rejectsIncompleteCycleClauses(String clause) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(CTE + clause + " SELECT * FROM walk")); + } + + @Test + void visitsCycleExpressions() throws Exception { + Select select = (Select) CCJSqlParserUtil.parse(CTE + + "CYCLE id SET flag TO 'Y' DEFAULT 'N' USING path SELECT * FROM walk"); + List values = new ArrayList<>(); + select.accept(new SelectVisitorAdapter(new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + values.add(value.getValue()); + return null; + } + }), null); + assertEquals(List.of("Y", "N"), values); + } + + @ParameterizedTest + @ValueSource(strings = {"TO upper('Y') DEFAULT 'N'", "TO 'Y' DEFAULT lower('N')"}) + void validatesCycleMarkExpressionsAlongsideTheCteBody(String markValues) { + String sql = CTE + "CYCLE id SET flag " + markValues + " USING path SELECT * FROM walk"; + assertTrue( + Validation.validate(List.of(new FeaturesAllowed(Feature.values())), sql).isEmpty()); + + FeaturesAllowed allowed = new FeaturesAllowed(Feature.values()).remove(Feature.function); + List errors = Validation.validate(List.of(allowed), sql); + assertEquals(1, errors.size()); + assertNotNull(errors.get(0).getParsedStatement()); + assertEquals(1, errors.get(0).getErrors().size()); + assertEquals("function not allowed.", + errors.get(0).getErrors().iterator().next().getMessage()); + } + + @Test + void sharesSearchAndCycleRenderingWithExpressionDeparser() throws Exception { + Select select = (Select) CCJSqlParserUtil.parse(CTE + + "SEARCH DEPTH FIRST BY id SET seq CYCLE id SET flag TO 'Y' DEFAULT 'N' USING path SELECT * FROM walk"); + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + getBuilder().append("changed_").append(column.getColumnName()); + return getBuilder(); + } + + @Override + public StringBuilder visit(StringValue value, S context) { + getBuilder().append("'changed_").append(value.getValue()).append("'"); + return getBuilder(); + } + }; + select.accept(new StatementDeParser(expressions, new SelectDeParser(), sql), null); + assertTrue(sql.toString().contains( + "SEARCH DEPTH FIRST BY changed_id SET seq CYCLE changed_id SET flag TO 'changed_Y' DEFAULT 'changed_N' USING path"), + sql.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/WithItemTest.java b/src/test/java/net/sf/jsqlparser/statement/select/WithItemTest.java index f84bfa4b7c..ecc405a977 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/WithItemTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/WithItemTest.java @@ -10,16 +10,117 @@ package net.sf.jsqlparser.statement.select; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserConstants; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.ParseException; +import net.sf.jsqlparser.parser.Token; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; class WithItemTest { + @ParameterizedTest + @CsvSource({"2047, 1", "2047, -1", "2048, 1", "4096, 1"}) + void testLargeExpressionAlias(int arguments, String firstArgument) throws JSQLParserException { + String expression = "coalesce(" + firstArgument + ", 1".repeat(arguments - 1) + ")"; + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH " + expression + " AS v, t AS (SELECT v) SELECT v FROM t", true); + WithItem item = select.getWithItemsList().get(0); + Function function = assertInstanceOf(Function.class, item.getExpression()); + assertEquals("v", item.getAliasName()); + assertNull(item.getParenthesedStatement()); + assertEquals("coalesce", function.getName()); + assertEquals(arguments, function.getParameters().size()); + assertEquals(firstArgument, function.getParameters().get(0).toString()); + assertEquals("1", function.getParameters().get(arguments - 1).toString()); + assertNotNull(select.getWithItemsList().get(1).getParenthesedStatement()); + } + + @ParameterizedTest + @ValueSource(ints = {2047, 2048}) + void testLargeCteColumnList(int columns) throws JSQLParserException { + String names = IntStream.range(0, columns).mapToObj(i -> "c" + i) + .collect(Collectors.joining(", ")); + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH t(" + names + ") AS (SELECT 1" + ", 1".repeat(columns - 1) + + ") SELECT * FROM t", + true); + WithItem item = select.getWithItemsList().get(0); + assertEquals("t", item.getAliasName()); + assertNull(item.getExpression()); + assertNotNull(item.getParenthesedStatement()); + assertEquals(columns, item.getWithItemList().size()); + assertEquals("c" + (columns - 1), item.getWithItemList().get(columns - 1).toString()); + } + + @Test + void testLargeNestedExpressionAlias() throws JSQLParserException { + String expression = "concat(concat('(', ')')" + ", 'x'".repeat(2047) + ")"; + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH " + expression + " AS v SELECT v", true); + Function function = assertInstanceOf(Function.class, + select.getWithItemsList().get(0).getExpression()); + assertEquals(2048, function.getParameters().size()); + assertEquals("concat('(', ')')", function.getParameters().get(0).toString()); + } + + @ParameterizedTest + @ValueSource(ints = {1, 64, 65}) + void testQualifiedExpressionAlias(int qualifiers) throws JSQLParserException { + String name = "s.".repeat(qualifiers) + "coalesce"; + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH " + name + "(1, 2) AS v SELECT v", true); + Function function = assertInstanceOf(Function.class, + select.getWithItemsList().get(0).getExpression()); + assertEquals(name, function.getName()); + assertEquals(2, function.getParameters().size()); + } + + @ParameterizedTest + @ValueSource(ints = {2, 2048}) + @Timeout(5) + void testUnclosedExpressionAlias(int arguments) { + String sql = "WITH coalesce(1" + ", 1".repeat(arguments - 1); + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Statement()); + } + + @Test + void testInterruptedWithItemLookaheadStopsReading() { + CCJSqlParser parser = CCJSqlParserUtil.newParser( + "WITH coalesce(1" + ", 1".repeat(512) + ") AS v SELECT v"); + AtomicBoolean reachedAlias = new AtomicBoolean(); + parser.token_source = Mockito.spy(parser.token_source); + Mockito.doAnswer(invocation -> { + Token token = (Token) invocation.callRealMethod(); + if (token.kind == CCJSqlParserConstants.S_LONG) { + parser.interrupted = true; + } else if (token.kind == CCJSqlParserConstants.K_AS) { + reachedAlias.set(true); + } + return token; + }).when(parser.token_source).getNextToken(); + assertThrows(CancellationException.class, parser::Statement); + assertFalse(reachedAlias.get()); + } + @Test void testNotMaterializedIssue2251() throws JSQLParserException { String sqlStr = "WITH devices AS NOT MATERIALIZED (\n" diff --git a/src/test/java/net/sf/jsqlparser/statement/select/XmlForestTest.java b/src/test/java/net/sf/jsqlparser/statement/select/XmlForestTest.java new file mode 100644 index 0000000000..c7cc4ddb08 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/XmlForestTest.java @@ -0,0 +1,105 @@ +/*- + * #%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.statement.select; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNotAllowed; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.AliasedExpression; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class XmlForestTest { + @ParameterizedTest + @ValueSource(strings = {"XMLFOREST(name AS \"Name\")", "xmlforest(a, b AS item)", + "XMLFOREST(1 + 2 AS total, COALESCE(a, 'default') AS \"Value\")", + "XMLFOREST(XMLFOREST(a AS inner_name) AS outer_name)", + "XMLFOREST(name a)", "XMLFOREST(name)", "XMLPARSE(CONTENT a)"}) + void parsesAndRoundTripsNamedAndUnnamedArguments(String expression) throws Exception { + var statement = assertSqlCanBeParsedAndDeparsed("SELECT " + expression + " FROM t"); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(statement.toString()).toString()); + } + + @Test + void aliasIsStructuredAndCanBeEditedWithoutReparsingSql() throws Exception { + var statement = + (PlainSelect) CCJSqlParserUtil.parse("SELECT XMLFOREST(name AS \"Name\") FROM t"); + Function function = (Function) statement.getSelectItem(0).getExpression(); + AliasedExpression argument = (AliasedExpression) function.getParameters().get(0); + assertEquals("name", ((Column) argument.getExpression()).getColumnName()); + assertEquals("\"Name\"", argument.getAlias().getName()); + argument.setExpression(new LongValue(7)); + argument.setAlias(new Alias("value", true)); + assertEquals("SELECT XMLFOREST(7 AS value) FROM t", statement.toString()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + } + + @Test + void customVisitorReachesValuesWithContextAndDeparserKeepsAliases() throws Exception { + var statement = (PlainSelect) CCJSqlParserUtil + .parse("SELECT XMLFOREST(1 + 2 AS total, 3 AS n) FROM t"); + List values = new ArrayList<>(); + statement.getSelectItem(0).getExpression().accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }, "context"); + assertEquals(Arrays.asList(1L, 2L, 3L), values); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals("SELECT XMLFOREST(101 + 102 AS total, 103 AS n) FROM t", output.toString()); + } + + @Test + void traversalAndValidationReachTheUnderlyingArgument() throws Exception { + assertEquals(Set.of("outer_t", "inner_t"), TablesNamesFinder.findTables( + "SELECT XMLFOREST((SELECT a FROM inner_t) AS item) FROM outer_t")); + validateNotAllowed("SELECT a FROM t WHERE XMLFOREST(? AS item) IS NULL", 1, 1, + new FeaturesAllowed().add(FeaturesAllowed.SELECT).remove(Feature.jdbcParameter), + Feature.jdbcParameter); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT XMLFOREST(a AS) FROM t", "SELECT XMLFOREST(a AS b.c) FROM t", + "SELECT f(a AS b) FROM t"}) + void rejectsMissingAliasesAndKeepsOrdinaryFunctionArgumentsStrict(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/show/ShowCreateStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/show/ShowCreateStatementTest.java new file mode 100644 index 0000000000..2d82488e10 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/show/ShowCreateStatementTest.java @@ -0,0 +1,212 @@ +/*- + * #%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.statement.show; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementFeatures; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.statement.show.ShowCreateStatement.ObjectType; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ShowCreateStatementTest { + + private static final List> DIALECTS = List.of(parser -> { + }, parser -> parser.withDialect(Dialect.MYSQL), + parser -> parser.withDialect(Dialect.MARIADB)); + + /** Parses without a dialect and with MySQL and MariaDB, then deparses and reparses. */ + private static ShowCreateStatement roundTrip(String sql) throws JSQLParserException { + ShowCreateStatement result = null; + for (Consumer dialect : DIALECTS) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect); + assertInstanceOf(ShowCreateStatement.class, statement); + assertDeparse(statement, sql); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString()); + result = (ShowCreateStatement) statement; + } + return result; + } + + @Test + void createTable() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE TABLE c20"); + + assertEquals(ObjectType.TABLE, show.getObjectType()); + assertEquals("c20", show.getName().getFullyQualifiedName()); + assertNull(show.getDatabaseName()); + assertNull(show.getAccount()); + } + + @ParameterizedTest + @CsvSource({ + "SHOW CREATE TABLE `db`.`t`, TABLE, `db`.`t`", + "SHOW CREATE VIEW db.v, VIEW, db.v", + "SHOW CREATE PROCEDURE gp13, PROCEDURE, gp13", + "SHOW CREATE FUNCTION db.f, FUNCTION, db.f", + "SHOW CREATE TRIGGER tr, TRIGGER, tr", + "SHOW CREATE EVENT db.e, EVENT, db.e" + }) + void namedObjects(String sql, ObjectType type, String name) throws JSQLParserException { + ShowCreateStatement show = roundTrip(sql); + + assertEquals(type, show.getObjectType()); + assertTrue(type.isNamedByTable()); + assertEquals(name, show.getName().getFullyQualifiedName()); + } + + @Test + void createDatabase() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE DATABASE `shop`"); + + assertEquals(ObjectType.DATABASE, show.getObjectType()); + assertFalse(show.isSchemaKeyword()); + assertFalse(show.isIfNotExists()); + assertEquals("`shop`", show.getDatabaseName()); + assertNull(show.getName()); + } + + @Test + void createSchemaIfNotExists() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE SCHEMA IF NOT EXISTS shop"); + + assertEquals(ObjectType.DATABASE, show.getObjectType()); + assertTrue(show.isSchemaKeyword()); + assertTrue(show.isIfNotExists()); + assertEquals("shop", show.getDatabaseName()); + } + + @Test + void createUserWithHost() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE USER 'app'@'%'"); + + assertEquals(ObjectType.USER, show.getObjectType()); + assertEquals("app", show.getAccount().getUser().getNotExcapedValue()); + assertEquals("%", show.getAccount().getHost().getNotExcapedValue()); + assertNull(show.getAccount().getAuthentication()); + assertFalse(show.isCurrentUser()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW CREATE USER app", + "SHOW CREATE USER 'app'", + "SHOW CREATE USER `app`@`localhost`", + "SHOW CREATE USER app@localhost", + "SHOW CREATE USER app@'%'", + "SHOW CREATE USER 'app'@localhost", + "SHOW CREATE USER \"app\"@\"%\"" + }) + void accountSpellings(String sql) throws JSQLParserException { + ShowCreateStatement show = roundTrip(sql); + + assertEquals(ObjectType.USER, show.getObjectType()); + assertFalse(show.isCurrentUser()); + assertEquals("app", show.getAccount().getUser().getValue()); + } + + @Test + void currentUser() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE USER CURRENT_USER"); + assertTrue(show.isCurrentUser()); + assertFalse(show.isCurrentUserParentheses()); + + show = roundTrip("SHOW CREATE USER CURRENT_USER()"); + assertTrue(show.isCurrentUser()); + assertTrue(show.isCurrentUserParentheses()); + assertNull(show.getAccount().getHost()); + } + + @Test + void keywordsAreCaseInsensitive() throws JSQLParserException { + assertEquals("SHOW CREATE SCHEMA IF NOT EXISTS db", + CCJSqlParserUtil.parse("show create schema if not exists db").toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW CREATE TEMPORARY TABLE t", + "SHOW CREATE TABLE t FORMAT TabSeparated", + "SHOW CREATE TABLE t AS SERDE", + "SHOW CREATE SEQUENCE s", + "SHOW CREATE PACKAGE BODY p", + "SHOW CREATE VIEW IF NOT EXISTS v", + "SHOW CREATE DATABASE IF NOT EXISTS", + "SHOW CREATE USER u IDENTIFIED BY 'x'", + "SHOW CREATE TABLE" + }) + void otherFormsRemainUnsupported(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + assertInstanceOf(UnsupportedStatement.class, statement); + } + + @Test + void endsAtTheStatementBoundary() throws JSQLParserException { + Statements statements = CCJSqlParserUtil.parseStatements( + "SHOW CREATE TABLE a;\nSHOW CREATE USER CURRENT_USER();\nSELECT 1", + parser -> parser.withDialect(Dialect.MYSQL)); + + assertEquals(3, statements.size()); + assertInstanceOf(ShowCreateStatement.class, statements.get(0)); + assertInstanceOf(ShowCreateStatement.class, statements.get(1)); + } + + @Test + void findsTablesAndViewsOnly() throws JSQLParserException { + assertThat(TablesNamesFinder.findTables("SHOW CREATE TABLE s.t")).containsExactly("s.t"); + assertThat(TablesNamesFinder.findTables("SHOW CREATE VIEW v")).containsExactly("v"); + assertThat(TablesNamesFinder.findTables("SHOW CREATE TRIGGER tr")).isEmpty(); + assertThat(TablesNamesFinder.findTables("SHOW CREATE DATABASE db")).isEmpty(); + assertThat(TablesNamesFinder.findTables("SHOW CREATE USER u")).isEmpty(); + } + + @Test + void buildsAndDeparses() { + assertDeparse(new ShowCreateStatement(ObjectType.VIEW, new Table("s", "v")), + "SHOW CREATE VIEW s.v"); + assertDeparse(new ShowCreateStatement().withObjectType(ObjectType.DATABASE) + .withSchemaKeyword(true).withIfNotExists(true).withDatabaseName("db"), + "SHOW CREATE SCHEMA IF NOT EXISTS db"); + UserAccount account = new UserAccount().withUser(new StringValue("'app'")) + .withHost(new StringValue("'localhost'")); + assertDeparse(new ShowCreateStatement().withObjectType(ObjectType.USER) + .withAccount(account), "SHOW CREATE USER 'app'@'localhost'"); + } + + @Test + void reportsMetadataFeatures() throws JSQLParserException { + StatementFeatures features = CCJSqlParserUtil.parse("SHOW CREATE TABLE t").getFeatures(); + + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatementTest.java new file mode 100644 index 0000000000..0905f32853 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatementTest.java @@ -0,0 +1,143 @@ +/*- + * #%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.statement.show; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.ShowStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementFeatures; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ShowTableStatusStatementTest { + + private static final List> DIALECTS = List.of(parser -> { + }, parser -> parser.withDialect(Dialect.MYSQL), + parser -> parser.withDialect(Dialect.MARIADB)); + + /** Parses without a dialect and with MySQL and MariaDB, then deparses and reparses. */ + private static ShowTableStatusStatement roundTrip(String sql) throws JSQLParserException { + ShowTableStatusStatement result = null; + for (Consumer dialect : DIALECTS) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect); + assertInstanceOf(ShowTableStatusStatement.class, statement); + assertDeparse(statement, sql); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString()); + result = (ShowTableStatusStatement) statement; + } + return result; + } + + @Test + void currentDatabase() throws JSQLParserException { + ShowTableStatusStatement show = roundTrip("SHOW TABLE STATUS"); + + assertNull(show.getDbName()); + assertNull(show.getLikeExpression()); + assertNull(show.getWhereCondition()); + } + + @Test + void likePattern() throws JSQLParserException { + ShowTableStatusStatement show = roundTrip("SHOW TABLE STATUS LIKE 'c0%'"); + + assertEquals("c0%", ((StringValue) show.getLikeExpression()).getValue()); + } + + @Test + void databaseAndWhere() throws JSQLParserException { + ShowTableStatusStatement show = roundTrip("SHOW TABLE STATUS FROM v WHERE Comment <> ''"); + + assertEquals(SelectionMode.FROM, show.getSelectionMode()); + assertEquals("v", show.getDbName()); + assertEquals("Comment <> ''", show.getWhereCondition().toString()); + } + + @Test + void inDatabaseAndLike() throws JSQLParserException { + ShowTableStatusStatement show = roundTrip("SHOW TABLE STATUS IN `db` LIKE 'a%'"); + + assertEquals(SelectionMode.IN, show.getSelectionMode()); + assertEquals("`db`", show.getDbName()); + } + + @Test + void keywordsAreCaseInsensitive() throws JSQLParserException { + assertEquals("SHOW TABLE STATUS IN db", + CCJSqlParserUtil.parse("show table status in db").toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW TABLE foo", + "SHOW TABLE EXTENDED IN db LIKE 'x'", + "SHOW TABLE STATUS FROM db LIMIT 3" + }) + void otherTableFormsRemainUnsupported(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + assertInstanceOf(UnsupportedStatement.class, statement); + } + + @Test + void neighbouringShowFormsAreUnchanged() throws JSQLParserException { + assertInstanceOf(ShowTablesStatement.class, CCJSqlParserUtil.parse("SHOW TABLES")); + assertInstanceOf(ShowStatement.class, CCJSqlParserUtil.parse("SHOW STATUS")); + assertSqlCanBeParsedAndDeparsed("SELECT status FROM t WHERE status = 1"); + } + + @Test + void endsAtTheStatementBoundary() throws JSQLParserException { + Statements statements = + CCJSqlParserUtil.parseStatements("SHOW TABLE STATUS;\nSHOW TABLES"); + + assertEquals(2, statements.size()); + assertInstanceOf(ShowTableStatusStatement.class, statements.get(0)); + } + + @Test + void namesNoTable() throws JSQLParserException { + assertThat(TablesNamesFinder.findTables("SHOW TABLE STATUS FROM db LIKE 't%'")).isEmpty(); + } + + @Test + void buildsAndDeparses() { + ShowTableStatusStatement show = new ShowTableStatusStatement() + .withSelectionMode(SelectionMode.IN).withDbName("db") + .withLikeExpression(new StringValue("t%")); + + assertDeparse(show, "SHOW TABLE STATUS IN db LIKE 't%'"); + } + + @Test + void reportsMetadataFeatures() throws JSQLParserException { + StatementFeatures features = CCJSqlParserUtil.parse("SHOW TABLE STATUS").getFeatures(); + + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/truncate/OracleTruncateTest.java b/src/test/java/net/sf/jsqlparser/statement/truncate/OracleTruncateTest.java new file mode 100644 index 0000000000..1c49a1aff9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/truncate/OracleTruncateTest.java @@ -0,0 +1,108 @@ +/*- + * #%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.statement.truncate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class OracleTruncateTest { + @ParameterizedTest + @ValueSource(strings = {"samples", "hr.\"Sample.Table\""}) + void preservesLogStorageAndCascadeOptions(String table) throws JSQLParserException { + for (Truncate.MaterializedViewLogOption log : new Truncate.MaterializedViewLogOption[] { + null, Truncate.MaterializedViewLogOption.PRESERVE, + Truncate.MaterializedViewLogOption.PURGE}) { + for (Truncate.StorageOption storage : new Truncate.StorageOption[] {null, + Truncate.StorageOption.DROP, Truncate.StorageOption.DROP_ALL, + Truncate.StorageOption.REUSE}) { + for (String cascade : new String[] {"", " CASCADE"}) { + String sql = "TRUNCATE TABLE " + table + + (log == null ? "" : " " + log + " MATERIALIZED VIEW LOG") + + (storage == null ? "" + : storage == Truncate.StorageOption.DROP_ALL + ? " DROP ALL STORAGE" + : " " + storage + " STORAGE") + + cascade; + for (Dialect dialect : new Dialect[] {null, Dialect.ORACLE}) { + Truncate truncate = parse(sql, dialect); + assertEquals(table, truncate.getTable().getFullyQualifiedName()); + assertEquals(log, truncate.getMaterializedViewLogOption()); + assertEquals(storage, truncate.getStorageOption()); + assertEquals(cascade.isEmpty() ? null : CascadeBehavior.CASCADE, + truncate.getDropBehavior()); + assertNull(truncate.getIdentityOption()); + assertEquals(sql, truncate.toString()); + roundTrip(truncate, dialect); + assertEquals(2, + CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + } + } + } + } + } + + @Test + void supportsModelConstructionAndClearingOptions() throws JSQLParserException { + Truncate truncate = new Truncate().withTableToken(true).withTable(new Table("samples")) + .withMaterializedViewLogOption(Truncate.MaterializedViewLogOption.PURGE) + .withStorageOption(Truncate.StorageOption.DROP_ALL).withCascade(true); + assertEquals("TRUNCATE TABLE samples PURGE MATERIALIZED VIEW LOG DROP ALL STORAGE CASCADE", + truncate.toString()); + roundTrip(truncate, Dialect.ORACLE); + truncate.setMaterializedViewLogOption(Truncate.MaterializedViewLogOption.PRESERVE); + truncate.setStorageOption(Truncate.StorageOption.REUSE); + assertEquals("TRUNCATE TABLE samples PRESERVE MATERIALIZED VIEW LOG REUSE STORAGE CASCADE", + truncate.toString()); + roundTrip(truncate, null); + truncate.setMaterializedViewLogOption(null); + truncate.setStorageOption(null); + assertEquals("TRUNCATE TABLE samples CASCADE", truncate.toString()); + roundTrip(truncate, null); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP", "DROP ALL", "REUSE", "PRESERVE MATERIALIZED VIEW", + "PURGE LOG", "DROP STORAGE REUSE STORAGE", + "REUSE STORAGE PURGE MATERIALIZED VIEW LOG", "CASCADE REUSE STORAGE"}) + void rejectsIncompleteRepeatedAndOutOfOrderOptions(String options) { + assertThrows(JSQLParserException.class, + () -> parse("TRUNCATE TABLE samples " + options, Dialect.ORACLE)); + } + + private static Truncate parse(String sql, Dialect dialect) throws JSQLParserException { + return (Truncate) CCJSqlParserUtil.parse(sql, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + } + + private static void roundTrip(Truncate truncate, Dialect dialect) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + truncate.accept(new StatementDeParser(output), null); + assertEquals(truncate.toString(), output.toString()); + Truncate reparsed = parse(output.toString(), dialect); + assertEquals(truncate.getMaterializedViewLogOption(), + reparsed.getMaterializedViewLogOption()); + assertEquals(truncate.getStorageOption(), reparsed.getStorageOption()); + assertEquals(output.toString(), reparsed.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/truncate/PostgreSqlTruncateTest.java b/src/test/java/net/sf/jsqlparser/statement/truncate/PostgreSqlTruncateTest.java new file mode 100644 index 0000000000..054dab64aa --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/truncate/PostgreSqlTruncateTest.java @@ -0,0 +1,105 @@ +/*- + * #%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.statement.truncate; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.lock.LockStatement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlTruncateTest { + @ParameterizedTest + @ValueSource(strings = {"t", "ONLY t", "t *", "ONLY (public.t)", + "ONLY t, public.u *", "t, ONLY u", "ONLY t, ONLY u"}) + void targetsAndOptionalClausesRoundTrip(String targets) throws JSQLParserException { + for (String identity : new String[] {"", " RESTART IDENTITY", " CONTINUE IDENTITY"}) { + for (String behavior : new String[] {"", " CASCADE", " RESTRICT"}) { + for (String tableKeyword : new String[] {"", " TABLE"}) { + String sql = "TRUNCATE" + tableKeyword + " " + targets + identity + behavior; + Truncate statement = (Truncate) CCJSqlParserUtil.parse(sql); + assertEquals(sql, statement.toString()); + assertEquals(targets.contains(",") ? 2 : 1, statement.getTargets().size()); + assertEquals(identity.contains("RESTART") ? Truncate.IdentityOption.RESTART + : identity.contains("CONTINUE") ? Truncate.IdentityOption.CONTINUE + : null, + statement.getIdentityOption()); + assertEquals(behavior.contains("CASCADE") ? CascadeBehavior.CASCADE + : behavior.contains("RESTRICT") ? CascadeBehavior.RESTRICT : null, + statement.getDropBehavior()); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + } + } + } + } + + @Test + void visitsEveryTableAndKeepsLegacyMutationsLive() throws JSQLParserException { + Truncate statement = (Truncate) CCJSqlParserUtil.parse( + "TRUNCATE ONLY a, b *, ONLY c RESTART IDENTITY CASCADE"); + assertEquals(Set.of("a", "b", "c"), new TablesNamesFinder().getTables(statement)); + assertEquals("c", statement.getTable().getName()); + statement.setTable(new Table("x")); + statement.getTables().set(1, new Table("y")); + assertTrue(statement.getTargets().get(1).isIncludeDescendants()); + statement.getTargets().get(2).setOnly(false); + statement.getTables().add(new Table("z")); + statement.setCascade(false); + statement.setDropBehavior(CascadeBehavior.RESTRICT); + assertFalse(statement.getCascade()); + assertEquals("TRUNCATE ONLY a, y *, x, z RESTART IDENTITY RESTRICT", statement.toString()); + assertEquals(Set.of("a", "y", "x", "z"), new TablesNamesFinder().getTables(statement)); + statement.getTables().remove(2); + roundTrip(statement); + statement.setTables(statement.getTables()); + assertEquals(3, statement.getTargets().size()); + statement.setTables(List.of(new Table("replacement"))); + assertTrue(statement.isOnly()); + assertEquals("replacement", statement.getTable().getName()); + } + + @Test + void lockAndTruncateShareRelationTargets() throws JSQLParserException { + LockStatement lock = (LockStatement) CCJSqlParserUtil.parse( + "LOCK TABLE ONLY (public.t), u * IN ACCESS SHARE MODE"); + assertEquals(LockStatement.Scope.ONLY, lock.getTargets().get(0).getScope()); + assertTrue(lock.getTargets().get(0).isParenthesized()); + assertEquals(LockStatement.Scope.INCLUDING_DESCENDANTS, + lock.getTargets().get(1).getScope()); + lock.getTargets().get(0).setScope(LockStatement.Scope.DEFAULT); + assertFalse(lock.getTargets().get(0).isParenthesized()); + roundTrip(lock); + } + + @ParameterizedTest + @ValueSource(strings = {"ONLY t *", "(t)", "t RESTART", "t CONTINUE", + "t RESTART IDENTITY CONTINUE IDENTITY", "t CASCADE RESTRICT", "t,", + "t RESTRICT RESTART IDENTITY"}) + void rejectsMalformedTargetsAndClauses(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse("TRUNCATE " + sql)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/truncate/TruncateMultipleTablesTest.java b/src/test/java/net/sf/jsqlparser/statement/truncate/TruncateMultipleTablesTest.java index 18894c8ca0..494a0cdec4 100644 --- a/src/test/java/net/sf/jsqlparser/statement/truncate/TruncateMultipleTablesTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/truncate/TruncateMultipleTablesTest.java @@ -13,7 +13,6 @@ import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.StringReader; @@ -86,8 +85,8 @@ public void testTruncateDeparse() throws JSQLParserException { String statement = "TRUNCATE TABLE foo, bar"; assertSqlCanBeParsedAndDeparsed(statement); assertDeparse(new Truncate() - .withTables(List.of(new Table("foo"), new Table("bar"))) - .withTableToken(true), statement); + .withTables(List.of(new Table("foo"), new Table("bar"))) + .withTableToken(true), statement); } @Test @@ -95,16 +94,18 @@ public void testTruncateCascadeDeparse() throws JSQLParserException { String statement = "TRUNCATE TABLE foo, bar CASCADE"; assertSqlCanBeParsedAndDeparsed(statement); assertDeparse(new Truncate() - .withTables(List.of(new Table("foo"), new Table("bar"))) - .withTableToken(true) - .withCascade(true), statement); + .withTables(List.of(new Table("foo"), new Table("bar"))) + .withTableToken(true) + .withCascade(true), statement); } @Test - public void testTruncateDoesNotAllowOnlyWithMultipleTables() { + public void testTruncateAllowsOnlyForAnIndividualTarget() throws JSQLParserException { String statement = "TRUNCATE TABLE ONLY foo, bar"; - assertThrows(JSQLParserException.class, - () -> parserManager.parse(new StringReader(statement))); + Truncate truncate = (Truncate) parserManager.parse(new StringReader(statement)); + assertEquals(statement, truncate.toString()); + assertTrue(truncate.getTargets().get(0).isOnly()); + assertEquals(2, truncate.getTargets().size()); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/truncate/TruncateTest.java b/src/test/java/net/sf/jsqlparser/statement/truncate/TruncateTest.java index 698d2a5b84..129b3f43f0 100644 --- a/src/test/java/net/sf/jsqlparser/statement/truncate/TruncateTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/truncate/TruncateTest.java @@ -18,7 +18,6 @@ import static net.sf.jsqlparser.test.TestUtils.assertDeparse; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -72,8 +71,8 @@ public void testTruncateDeparse() throws JSQLParserException { String statement = "TRUNCATE TABLE foo"; assertSqlCanBeParsedAndDeparsed(statement); assertDeparse(new Truncate() - .withTable(new Table("foo")) - .withTableToken(true), statement); + .withTable(new Table("foo")) + .withTableToken(true), statement); } @Test @@ -81,9 +80,9 @@ public void testTruncateCascadeDeparse() throws JSQLParserException { String statement = "TRUNCATE TABLE foo CASCADE"; assertSqlCanBeParsedAndDeparsed(statement); assertDeparse(new Truncate() - .withTable(new Table("foo")) - .withTableToken(true) - .withCascade(true), statement); + .withTable(new Table("foo")) + .withTableToken(true) + .withCascade(true), statement); } @Test @@ -91,9 +90,9 @@ public void testTruncateOnlyDeparse() throws JSQLParserException { String statement = "TRUNCATE TABLE ONLY foo"; assertSqlCanBeParsedAndDeparsed(statement); assertDeparse(new Truncate() - .withTable(new Table("foo")) - .withTableToken(true) - .withOnly(true), statement); + .withTable(new Table("foo")) + .withTableToken(true) + .withOnly(true), statement); } @Test @@ -101,16 +100,15 @@ public void testTruncateOnlyAndCascadeDeparse() throws JSQLParserException { String statement = "TRUNCATE ONLY foo CASCADE"; assertSqlCanBeParsedAndDeparsed(statement); assertDeparse(new Truncate() - .withTable(new Table("foo")) - .withCascade(true) - .withOnly(true), statement); + .withTable(new Table("foo")) + .withCascade(true) + .withOnly(true), statement); } @Test - public void throwsParseWhenOnlyUsedWithMultipleTables() { + public void onlyAppliesToItsOwnTarget() throws JSQLParserException { String statement = "TRUNCATE TABLE ONLY foo, bar"; - assertThrows(JSQLParserException.class, - () -> parserManager.parse(new StringReader(statement))); + assertSqlCanBeParsedAndDeparsed(statement); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/update/TeradataUpdateTest.java b/src/test/java/net/sf/jsqlparser/statement/update/TeradataUpdateTest.java new file mode 100644 index 0000000000..7c033fc057 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/update/TeradataUpdateTest.java @@ -0,0 +1,96 @@ +/*- + * #%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.statement.update; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TeradataUpdateTest { + private static Update parse(String sql) throws JSQLParserException { + return (Update) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.TERADATA)); + } + + @Test + void parsesIssue891AndFindsSourceTables() throws Exception { + String sql = "UPDATE a FROM db1.table1 a, db2.tabl2 b SET a.column1 = b.column1 " + + "WHERE a.column2 = b.column2"; + Update update = (Update) assertSqlCanBeParsedAndDeparsed(sql, true, + p -> p.withDialect(Dialect.TERADATA)); + assertTrue(update.isFromBeforeSet()); + assertTrue(update.isTargetTableAlias()); + assertEquals("a", update.getTable().getName()); + assertEquals(1, update.getJoins().size()); + assertEquals(Set.of("db1.table1", "db2.tabl2"), new TablesNamesFinder().getTables(update)); + assertEquals(sql, parse(update.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE t FROM source s SET id = s.id", + "UPDATE b FROM source a, target b SET b.id = a.id WHERE a.id > 1", + "UPDATE \"target\" FROM \"schema\".\"table\" AS \"target\" SET id = 1", + "UPDATE t SET id = 1"}) + void roundTripsTargetsAndOptionalFrom(String sql) throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, + p -> p.withDialect(Dialect.TERADATA)); + assertEquals(statement.toString(), parse(statement.toString()).toString()); + } + + @Test + void gatesClauseOrderAndRejectsDuplicates() throws Exception { + String sql = "UPDATE t FROM s SET id = 1"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.TERADATA) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect))); + } + } + assertThrows(JSQLParserException.class, () -> parse(sql + " FROM other")); + assertThrows(JSQLParserException.class, () -> parse("UPDATE t FROM SET id = 1")); + Update legacy = (Update) assertSqlCanBeParsedAndDeparsed("UPDATE t SET id = 1 FROM s"); + assertFalse(legacy.isFromBeforeSet()); + legacy.setFromBeforeSet(true); + assertEquals(sql, legacy.toString()); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1;", + p -> p.withDialect(Dialect.TERADATA)).size()); + } + + @Test + void customDeparserKeepsFromPosition() throws Exception { + Update update = parse("UPDATE t FROM s SET id = 1 WHERE s.id > 2"); + StringBuilder builder = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 10); + } + }; + update.accept(new StatementDeParser(expressions, new SelectDeParser(), builder)); + assertEquals("UPDATE t FROM s SET id = 11 WHERE s.id > 12", builder.toString()); + assertEquals(builder.toString(), parse(builder.toString()).toString()); + update.withFromBeforeSet(false); + assertEquals("UPDATE t SET id = 1 FROM s WHERE s.id > 2", update.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/update/UpdateSetParsingTest.java b/src/test/java/net/sf/jsqlparser/statement/update/UpdateSetParsingTest.java new file mode 100644 index 0000000000..7cdcf228a4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/update/UpdateSetParsingTest.java @@ -0,0 +1,189 @@ +/*- + * #%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.statement.update; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.merge.Merge; +import net.sf.jsqlparser.statement.merge.MergeUpdate; +import net.sf.jsqlparser.statement.piped.FromQuery; +import net.sf.jsqlparser.statement.piped.SetPipeOperator; +import net.sf.jsqlparser.statement.select.ParenthesedSelect; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class UpdateSetParsingTest { + + // These are consumers of the shared grammar, not claims of dialect-specific SQL validity. + enum EntryPoint { + UPDATE("UPDATE t SET "), INSERT_SET("INSERT INTO t SET "), INSERT_DUPLICATE( + "INSERT INTO t VALUES (0) ON DUPLICATE KEY UPDATE "), INSERT_CONFLICT( + "INSERT INTO t VALUES (0) ON CONFLICT (id) DO UPDATE SET "), MERGE_UPDATE( + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET "), UPSERT_SET( + "UPSERT INTO t SET "), PIPE_SET( + "FROM t |> SET "), SELECT_SETTINGS( + "SELECT * FROM t SETTINGS "); + + private final String prefix; + + EntryPoint(String prefix) { + this.prefix = prefix; + } + + List getUpdateSets(Statement statement) { + switch (this) { + case UPDATE: + return ((Update) statement).getUpdateSets(); + case INSERT_SET: + return ((Insert) statement).getSetUpdateSets(); + case INSERT_DUPLICATE: + return ((Insert) statement).getDuplicateAction().getUpdateSets(); + case INSERT_CONFLICT: + return ((Insert) statement).getConflictAction().getUpdateSets(); + case MERGE_UPDATE: + return ((MergeUpdate) ((Merge) statement).getOperations().get(0)) + .getUpdateSets(); + case UPSERT_SET: + return ((Upsert) statement).getUpdateSets(); + case PIPE_SET: + return ((SetPipeOperator) ((FromQuery) statement).getPipeOperators().get(0)) + .getUpdateSets(); + case SELECT_SETTINGS: + return ((PlainSelect) statement).getSettings(); + default: + throw new AssertionError(this); + } + } + } + + @ParameterizedTest + @EnumSource(EntryPoint.class) + void scalarAssignments(EntryPoint entryPoint) throws JSQLParserException { + assertAssignments(entryPoint, "a = 1, b = 2, c = 3", + new String[][] {{"a"}, {"b"}, {"c"}}, + new String[][] {{"1"}, {"2"}, {"3"}}); + } + + @ParameterizedTest + @EnumSource(EntryPoint.class) + void singleTupleAssignment(EntryPoint entryPoint) throws JSQLParserException { + assertAssignments(entryPoint, "(b, c) = (2, 3)", + new String[][] {{"b", "c"}}, new String[][] {{"2", "3"}}); + } + + @ParameterizedTest + @EnumSource(EntryPoint.class) + void tupleAssignmentFirst(EntryPoint entryPoint) throws JSQLParserException { + assertAssignments(entryPoint, "(b, c) = (2, 3), a = 1", + new String[][] {{"b", "c"}, {"a"}}, new String[][] {{"2", "3"}, {"1"}}); + } + + static Stream assignmentsAfterScalar() { + return Arrays.stream(EntryPoint.values()).flatMap(entryPoint -> Stream.of( + Arguments.of(entryPoint, "a = 1, (b, c) = (2, 3)", + new String[][] {{"a"}, {"b", "c"}}, + new String[][] {{"1"}, {"2", "3"}}), + Arguments.of(entryPoint, "a = 1, (b, c) = (2, 3), d = 4", + new String[][] {{"a"}, {"b", "c"}, {"d"}}, + new String[][] {{"1"}, {"2", "3"}, {"4"}}), + Arguments.of(entryPoint, "a = 1, (b, c) = (2, 3), (d, e) = (4, 5)", + new String[][] {{"a"}, {"b", "c"}, {"d", "e"}}, + new String[][] {{"1"}, {"2", "3"}, {"4", "5"}}), + Arguments.of(entryPoint, "a = 1, (b, c) = (COALESCE(b, 2), c + 3), d = ?", + new String[][] {{"a"}, {"b", "c"}, {"d"}}, + new String[][] {{"1"}, {"COALESCE(b, 2)", "c + 3"}, {"?"}}), + Arguments.of(entryPoint, "a = 1, (b, c) = (SELECT x, y FROM s), d = 4", + new String[][] {{"a"}, {"b", "c"}, {"d"}}, + new String[][] {{"1"}, {"(SELECT x, y FROM s)"}, {"4"}}))); + } + + @ParameterizedTest + @MethodSource("assignmentsAfterScalar") + void tupleAssignmentsAfterScalar(EntryPoint entryPoint, String assignments, + String[][] columns, String[][] values) throws JSQLParserException { + assertAssignments(entryPoint, assignments, columns, values); + } + + @Test + void tupleSubqueryAndFollowingClauses() throws JSQLParserException { + String sql = "UPDATE t SET a = DEFAULT, (b, c) = (SELECT x, y FROM s WHERE s.id = t.id), " + + "d = 4 WHERE t.id = 1 RETURNING a, b, c, d"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql); + assertUpdateSets(EntryPoint.UPDATE.getUpdateSets(statement), + new String[][] {{"a"}, {"b", "c"}, {"d"}}, + new String[][] {{"DEFAULT"}, {"(SELECT x, y FROM s WHERE s.id = t.id)"}, {"4"}}); + Update update = (Update) statement; + assertEquals("t.id = 1", update.getWhere().toString()); + assertEquals(" RETURNING a, b, c, d", update.getReturningClause().toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"a = 1 (b, c) = (2, 3)", "(b, c) = (2, 3) (d, e) = (4, 5)", + "a = 1,", "a = 1,, (b, c) = (2, 3)"}) + void rejectsMissingOrExtraSeparators(String assignments) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(EntryPoint.UPDATE.prefix + assignments)); + } + + private static void assertAssignments(EntryPoint entryPoint, String assignments, + String[][] columns, String[][] values) throws JSQLParserException { + Statement statement = + assertSqlCanBeParsedAndDeparsed(entryPoint.prefix + assignments, true); + assertUpdateSets(entryPoint.getUpdateSets(statement), columns, values); + assertUpdateSets(entryPoint.getUpdateSets(CCJSqlParserUtil.parse(statement.toString())), + columns, values); + StringBuilder builder = new StringBuilder(); + statement.accept(new StatementDeParser(builder)); + assertUpdateSets(entryPoint.getUpdateSets(CCJSqlParserUtil.parse(builder.toString())), + columns, values); + } + + private static void assertUpdateSets(List updateSets, String[][] columns, + String[][] values) { + assertEquals(columns.length, updateSets.size()); + for (int i = 0; i < columns.length; i++) { + UpdateSet updateSet = updateSets.get(i); + assertEquals(columns[i].length, updateSet.getColumns().size()); + assertEquals(values[i].length, updateSet.getValues().size()); + for (int j = 0; j < columns[i].length; j++) { + assertEquals(columns[i][j], updateSet.getColumn(j).getColumnName()); + } + for (int j = 0; j < values[i].length; j++) { + assertEquals(values[i][j], updateSet.getValue(j).toString()); + } + if (columns[i].length > 1) { + assertInstanceOf(ParenthesedExpressionList.class, updateSet.getColumns()); + if (values[i][0].startsWith("(SELECT")) { + assertInstanceOf(ParenthesedSelect.class, updateSet.getValue(0)); + } else { + assertInstanceOf(ParenthesedExpressionList.class, updateSet.getValues()); + } + } + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/test/TestUtils.java b/src/test/java/net/sf/jsqlparser/test/TestUtils.java index dff34d63fe..ffa71fa4cb 100644 --- a/src/test/java/net/sf/jsqlparser/test/TestUtils.java +++ b/src/test/java/net/sf/jsqlparser/test/TestUtils.java @@ -287,11 +287,9 @@ protected void appendDetail(final StringBuffer buffer, final String fieldName, @Override protected boolean accept(Class clazz) { - if (includingASTNode) { - return super.accept(clazz); - } else { - return isNotANode(clazz) && super.accept(clazz); - } + // Numeric values have value semantics; do not reflect into encapsulated JDK internals. + return !Number.class.isAssignableFrom(clazz) + && (includingASTNode || isNotANode(clazz)) && super.accept(clazz); } public boolean isNotANode(Class clazz) { diff --git a/src/test/java/net/sf/jsqlparser/util/ActiveTableDefinitionTraversalTest.java b/src/test/java/net/sf/jsqlparser/util/ActiveTableDefinitionTraversalTest.java new file mode 100644 index 0000000000..58e982c23c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/ActiveTableDefinitionTraversalTest.java @@ -0,0 +1,251 @@ +/*- + * #%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.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterOperation; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetDefault; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.create.table.KeyElement; +import net.sf.jsqlparser.statement.create.table.TableElement; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation; +import net.sf.jsqlparser.util.validation.validator.AlterValidator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ActiveTableDefinitionTraversalTest { + @ParameterizedTest + @ValueSource(strings = {"FOREIGN KEY (parent_id) REFERENCES old_parent (id)", + "INDEX old_index (old_id)", "CONSTRAINT hidden_check CHECK (old_id > 0)"}) + void addedColumnHidesStoredDefinitionFromAllConsumers(String definition) throws Exception { + Alter alter = parse("ALTER TABLE child ADD " + definition); + AlterExpression action = first(alter); + TableElement original = action.getActiveTableDefinition(); + action.addColDataType("added", new ColDataType("INT")); + assertEquals("ALTER TABLE child ADD added INT", alter.toString()); + assertNull(action.getActiveTableDefinition()); + assertTrue(original == action.getConstraint() || original == action.getIndex()); + assertEquals(Set.of("child"), new TablesNamesFinder<>().getTables(alter)); + assertTrue(expressions(action).isEmpty()); + assertEquals(List.of("child", "added"), validatedNames(alter, Set.of("child"))); + assertRoundTrip(alter, null); + } + + @Test + void operationChangesKeepForeignKeyStoredButDoNotVisitIt() throws Exception { + Alter alter = parse("ALTER TABLE child ADD FOREIGN KEY (parent_id) REFERENCES parent (id)"); + AlterExpression action = first(alter); + Object original = action.getConstraint(); + action.setOperation(AlterOperation.DROP_FOREIGN_KEY); + action.setConstraintName("fk"); + assertSame(original, action.getConstraint()); + assertNull(action.getActiveTableDefinition()); + assertEquals(Set.of("child"), new TablesNamesFinder<>().getTables(alter)); + assertEquals(List.of("child", "fk"), validatedNames(alter, Set.of("child", "fk"))); + assertRoundTrip(alter, Dialect.MYSQL); + } + + @Test + void activeForeignKeyStillVisitsItsTableAndBothColumnLists() throws Exception { + Alter alter = parse("ALTER TABLE child ADD FOREIGN KEY (parent_id) REFERENCES parent (id)"); + assertEquals(Set.of("child", "parent"), new TablesNamesFinder<>().getTables(alter)); + assertEquals(List.of("child", "parent", "id", "parent_id"), + validatedNames(alter, Set.of("child", "parent", "id", "parent_id"))); + assertRoundTrip(alter, null); + } + + @Test + void storedTableRenameDestinationIsInactiveWhenIndexNamesTakePrecedence() throws Exception { + Alter alter = parse("ALTER TABLE child RENAME INDEX original TO replacement"); + AlterExpression action = first(alter); + action.setOperation(AlterOperation.RENAME_TABLE); + action.setNewTable(new Table("hidden_table")); + assertFalse(alter.toString().contains("hidden_table")); + assertEquals(Set.of("child"), new TablesNamesFinder<>().getTables(alter)); + assertEquals("hidden_table", action.getNewTable().getName()); + } + + @Test + void defaultTraversalUsesTheSameBodyAsTheRenderer() throws Exception { + Alter alter = parse("ALTER TABLE child ALTER COLUMN id SET DEFAULT 1"); + AlterExpression action = first(alter); + ColumnDataType inactive = new ColumnDataType("hidden", false, new ColDataType("INT"), null); + inactive.addColumnOptions(ColumnOption.defaultValue(new LongValue(2))); + action.addColDataType(inactive); + assertEquals(List.of("1"), expressions(action)); + assertFalse(action.hasActiveColumnDefinitions()); + assertRoundTrip(alter, null); + } + + @Test + void dropSubclassCanSelectTheGeneralBodyAfterMutation() throws Exception { + Alter alter = parse("ALTER TABLE child DROP INDEX old_index"); + AlterExpression action = first(alter); + action.setOperation(AlterOperation.ADD); + action.setConstraint(first(parse("ALTER TABLE child ADD FOREIGN KEY (parent_id) " + + "REFERENCES parent (id)")).getConstraint()); + action.addColSetDefault(ColumnSetDefault.fromExpression("hidden", new LongValue(99))); + assertSame(action.getConstraint(), action.getActiveTableDefinition()); + assertEquals(Set.of("child", "parent"), new TablesNamesFinder<>().getTables(alter)); + assertTrue(expressions(action).isEmpty()); + assertEquals(List.of("child", "parent", "id", "parent_id"), + validatedNames(alter, Set.of("child", "parent", "id", "parent_id"))); + assertRoundTrip(alter, null); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "ALTER FOREIGN TABLE child OPTIONS (ADD optname 'active')|POSTGRESQL", + "ALTER TABLE child OWNER TO new_owner|POSTGRESQL", + "ALTER TABLE child ORDER BY active_id|MYSQL"}) + void fixedBodiesIgnoreInheritedDefinitionsAndDefaults(String sql, Dialect dialect) + throws Exception { + Alter alter = (Alter) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + AlterExpression action = first(alter); + List expectedExpressions = expressions(action); + action.setConstraint(first(parse("ALTER TABLE child ADD FOREIGN KEY (hidden_id) " + + "REFERENCES hidden_parent (id)")).getConstraint()); + action.addColDataType("hidden", new ColDataType("INT")); + action.addColSetDefault(ColumnSetDefault.fromExpression("hidden", new LongValue(99))); + action.setConstraintType("UNIQUE"); + action.setConstraintSymbol("hidden_key"); + action.setOperation(AlterOperation.RENAME_TABLE); + action.setNewTable(new Table("hidden_table")); + assertNull(action.getActiveTableDefinition()); + assertEquals(expectedExpressions, expressions(action)); + assertEquals(Set.of("child"), new TablesNamesFinder<>().getTables(alter)); + assertTrue(validatedNames(alter, Set.of("child", "active_id")).stream() + .noneMatch(name -> name.startsWith("hidden"))); + assertRoundTrip(alter, dialect); + } + + @Test + void identityAlterationDoesNotVisitDormantColumnOptionsOrUsingExpression() throws Exception { + Alter alter = (Alter) CCJSqlParserUtil.parse("ALTER TABLE child ALTER COLUMN id RESTART", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + ColumnDataType column = first(alter).getColDataTypeList().get(0); + column.addColumnOptions(ColumnOption.defaultValue(new LongValue(99))); + column.setUsingExpression(new LongValue(100)); + assertEquals("100", column.getUsingExpression().toString()); + assertTrue(expressions(first(alter)).isEmpty()); + assertRoundTrip(alter, Dialect.POSTGRESQL); + } + + @Test + void legacyConstraintHeaderVisitsKeysWithoutDormantStorageOptions() throws Exception { + Alter alter = parse("ALTER TABLE child ADD CONSTRAINT UNIQUE KEY uq (id)"); + AlterExpression action = first(alter); + KeyConstraint key = (KeyConstraint) action.getConstraint(); + key.setColumns(List.of(new KeyElement(new LongValue(7)))); + key.setStorageParameters(List.of(new IndexOption("hidden", new LongValue(99), true))); + assertTrue(action.hasActiveConstraintHeader()); + assertSame(key, action.getActiveTableDefinition()); + assertEquals(List.of("7"), expressions(action)); + assertFalse(alter.toString().contains("99")); + } + + @Test + void specializedPrimaryKeyKeepsActiveKeysAndHashOptions() throws Exception { + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE child ALTER PRIMARY KEY USING COLUMNS (id) " + + "USING HASH WITH BUCKET_COUNT = 8 WITH (fillfactor = 70)", + parser -> parser.withDialect(Dialect.COCKROACHDB)); + assertEquals(List.of("70", "8"), expressions(first(alter))); + assertSame(first(alter).getConstraint(), first(alter).getActiveTableDefinition()); + assertRoundTrip(alter, Dialect.COCKROACHDB); + } + + @ParameterizedTest + @ValueSource(strings = {"CHECK (hidden_id > 0)", "REFERENCES hidden_parent (id)"}) + void columnOptionKindChangesHidePriorConstraintPayload(String optionSql) throws Exception { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE FOREIGN TABLE child (id INT " + optionSql + ") SERVER srv"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + ColumnOption option = column.getColumnOptions().get(0); + Object stored = option.getConstraint() != null ? option.getConstraint() + : option.getForeignKeyReference(); + option.setForeignOptions( + List.of(new ForeignDataOption("column_name", new StringValue("active")))); + assertTrue(stored == option.getConstraint() || stored == option.getForeignKeyReference()); + List actual = new ArrayList<>(); + TableDefinitionTraversal.visit(table, expression -> actual.add(expression.toString()), + source -> fail("Inactive reference visited: " + source)); + assertEquals(List.of("'active'"), actual); + assertEquals(Set.of("child"), new TablesNamesFinder<>().getTables(table)); + assertRoundTrip(table, Dialect.POSTGRESQL); + } + + private static Alter parse(String sql) throws Exception { + return (Alter) CCJSqlParserUtil.parse(sql); + } + + private static AlterExpression first(Alter alter) { + return alter.getAlterExpressions().get(0); + } + + private static List expressions(AlterExpression action) { + List result = new ArrayList<>(); + TableDefinitionTraversal.visit(action, expression -> result.add(expression.toString()), + table -> { + }); + return result; + } + + private static List validatedNames(Alter alter, Set existing) { + List names = new ArrayList<>(); + DatabaseMetaDataValidation metadata = name -> { + names.add(name.getFqn()); + return existing.contains(name.getFqn()); + }; + AlterValidator validator = new AlterValidator(); + validator.setContext(new ValidationContext().setCapabilities(List.of(metadata))); + validator.validate(alter); + assertTrue(validator.getValidationErrors().isEmpty(), + validator.getValidationErrors().toString()); + return names; + } + + private static void assertRoundTrip(Statement statement, Dialect dialect) throws Exception { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql)); + assertEquals(statement.toString(), sql.toString()); + Statement reparsed = CCJSqlParserUtil.parse(sql.toString(), parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + assertEquals(sql.toString(), reparsed.toString()); + assertEquals(new TablesNamesFinder<>().getTables(reparsed), + new TablesNamesFinder<>().getTables(statement)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/AddAliasesVisitorTest.java b/src/test/java/net/sf/jsqlparser/util/AddAliasesVisitorTest.java index 6a8a919e54..d0975bc50a 100644 --- a/src/test/java/net/sf/jsqlparser/util/AddAliasesVisitorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/AddAliasesVisitorTest.java @@ -15,8 +15,10 @@ import org.junit.jupiter.api.Test; import java.io.StringReader; +import java.time.Duration; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; public class AddAliasesVisitorTest { @@ -68,4 +70,24 @@ public void testVisit_SetOperationList() throws JSQLParserException { assertEquals("SELECT 3 + 4 AS A1 FROM test UNION SELECT 7 + 8 AS A1 FROM test2", setOpList.toString()); } + + @Test + public void testManySelectItemsDoNotScaleQuadratically() throws JSQLParserException { + // Generated aliases were checked against the already-used ones with a linear + // LinkedList.contains, so adding aliases to a select with m items cost O(m^2). + final int m = 50000; + final StringBuilder sb = new StringBuilder("SELECT "); + for (int i = 0; i < m; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append("c").append(i); + } + sb.append(" FROM t"); + + // Parsing is left outside the timed region; only the alias walk is measured. + final Select select = (Select) parserManager.parse(new StringReader(sb.toString())); + assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> select.accept(new AddAliasesVisitor<>(), null)); + } } diff --git a/src/test/java/net/sf/jsqlparser/util/ConnectExpressionsVisitorTest.java b/src/test/java/net/sf/jsqlparser/util/ConnectExpressionsVisitorTest.java index 712e1950f6..827dd56a28 100644 --- a/src/test/java/net/sf/jsqlparser/util/ConnectExpressionsVisitorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/ConnectExpressionsVisitorTest.java @@ -18,8 +18,10 @@ import org.junit.jupiter.api.Test; import java.io.StringReader; +import java.time.Duration; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; public class ConnectExpressionsVisitorTest { @@ -54,4 +56,28 @@ protected BinaryExpression createBinaryExpression() { assertEquals("SELECT a + b + c AS testexpr FROM test", select.toString()); } + + @Test + public void testVisit_PlainSelect_manyItemsIsLinear() throws JSQLParserException { + int itemCount = 200000; + StringBuilder sql = new StringBuilder("select "); + for (int i = 0; i < itemCount; i++) { + if (i > 0) { + sql.append(','); + } + sql.append('c').append(i); + } + sql.append(" from test"); + Select select = (Select) parserManager.parse(new StringReader(sql.toString())); + ConnectExpressionsVisitor instance = new ConnectExpressionsVisitor<>() { + @Override + protected BinaryExpression createBinaryExpression() { + return new Concat(); + } + }; + // itemsExpr is a LinkedList; connecting the items used to index it by position (get(i)), + // which is O(n^2) in the number of select items. + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> select.accept(instance, null)); + assertEquals(1, select.getPlainSelect().getSelectItems().size()); + } } diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderCompletenessTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderCompletenessTest.java new file mode 100644 index 0000000000..d05a56d695 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderCompletenessTest.java @@ -0,0 +1,91 @@ +/*- + * #%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.util; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +/** + * TablesNamesFinder collects tables by dispatching through the visitor interfaces it declares. Most + * dispatch methods are abstract, so a missing implementation fails the build already. But the + * interfaces progressively gain default methods (e.g. SelectVisitor#visit(PivotQuery, context) + * returns null), and for those a missing TablesNamesFinder implementation compiles silently and + * loses the tables of that node type without any test failing. + * + * This test fails the build when a declared visitor interface has a dispatch method visit(Node, + * context) for an AST node that is not implemented in the TablesNamesFinder class hierarchy (it + * resolves to an interface default instead). It does not catch empty-shell implementations (an + * override that visits nothing) nor visitor families the finder does not declare at all: both + * remain manual review duties. + */ +public class TablesNamesFinderCompletenessTest { + + // GroupByElement: ExpressionVisitor's default forwards the group-by expressions and the + // grouping sets to the visitor, TablesNamesFinder relies on it from visit(PlainSelect). + private static final Set INHERITED_DISPATCH_NODES = + Set.of("net.sf.jsqlparser.statement.select.GroupByElement"); + + @Test + void testImplementsEveryDispatchMethodOfEveryDeclaredVisitorInterface() + throws NoSuchMethodException { + Class[] visitorInterfaces = TablesNamesFinder.class.getInterfaces(); + assertTrue(visitorInterfaces.length >= 8, "TablesNamesFinder must keep declaring the" + + " visitor families it dispatches through (currently 8), dropping one shrinks" + + " the completeness walk silently."); + + List gaps = new ArrayList<>(); + List interfacesWithoutDispatch = new ArrayList<>(); + for (Class visitorInterface : visitorInterfaces) { + int dispatchMethods = 0; + for (Method dispatch : visitorInterface.getMethods()) { + if (!dispatch.getName().equals("visit") || dispatch.getParameterCount() != 2) { + continue; + } + Class nodeType = dispatch.getParameterTypes()[0]; + // skips bulk dispatch helpers carrying a java.util collection + if (!nodeType.getName().startsWith("net.sf.jsqlparser.")) { + continue; + } + if (INHERITED_DISPATCH_NODES.contains(nodeType.getName())) { + continue; + } + dispatchMethods++; + Method implementation = TablesNamesFinder.class.getMethod(dispatch.getName(), + dispatch.getParameterTypes()); + if (implementation.getDeclaringClass().isInterface()) { + gaps.add(visitorInterface.getSimpleName() + "#" + dispatch.getName() + "(" + + nodeType.getSimpleName() + ", context)"); + } + } + if (dispatchMethods == 0) { + interfacesWithoutDispatch.add(visitorInterface.getSimpleName()); + } + } + + assertTrue(interfacesWithoutDispatch.isEmpty(), + () -> "No guarded dispatch method found for: " + + String.join(", ", interfacesWithoutDispatch) + ". Either the reflection" + + " walk lost the dispatch methods (filters too narrow, the completeness" + + " check would pass vacuously) or the interface is not a dispatch" + + " interface and must not be declared by TablesNamesFinder."); + assertTrue(gaps.isEmpty(), () -> "TablesNamesFinder must implement every dispatch" + + " method of the visitor interfaces it declares, otherwise the tables of that" + + " node type are silently not collected. Missing:\n" + String.join("\n", gaps) + + "\nImplement visit(Node, context) in TablesNamesFinder, or - when the" + + " inherited default already forwards every child - document the node in" + + " INHERITED_DISPATCH_NODES."); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index 8581a0e59b..551075b486 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java @@ -14,12 +14,17 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; @@ -27,9 +32,16 @@ import net.sf.jsqlparser.statement.DescribeStatement; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.select.AllColumns; +import net.sf.jsqlparser.statement.select.Join; +import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.statement.select.WithItem; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class TablesNamesFinderTest { @@ -367,23 +379,40 @@ public void testRemoteLink() throws JSQLParserException { } @Test - public void testCreateSequence_throwsException() throws JSQLParserException { + public void testCreateSequenceHasNoTableReferences() throws JSQLParserException { String sql = "CREATE SEQUENCE my_seq"; Statement stmt = CCJSqlParserUtil.parse(sql); TablesNamesFinder tablesNamesFinder = new TablesNamesFinder(); - assertThatThrownBy(() -> tablesNamesFinder.getTables(stmt)) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessage("Finding tables from CreateSequence is not supported"); + assertThat(tablesNamesFinder.getTables(stmt)).isEmpty(); } @Test - public void testAlterSequence_throwsException() throws JSQLParserException { + public void testAlterSequenceHasNoTableReferences() throws JSQLParserException { String sql = "ALTER SEQUENCE my_seq"; Statement stmt = CCJSqlParserUtil.parse(sql); TablesNamesFinder tablesNamesFinder = new TablesNamesFinder(); - assertThatThrownBy(() -> tablesNamesFinder.getTables(stmt)) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessage("Finding tables from AlterSequence is not supported"); + assertThat(tablesNamesFinder.getTables(stmt)).isEmpty(); + } + + @Test + public void testSetStatementFindsTablesInValues() throws JSQLParserException { + assertThat(TablesNamesFinder + .findTables("SET @a = (SELECT MAX(id) FROM MY_TABLE1), @b = 2")) + .containsExactlyInAnyOrder("MY_TABLE1"); + assertThat(TablesNamesFinder.findTables("SET search_path TO s1")).isEmpty(); + } + + @Test + public void testMySqlTriggerWithSetBody() throws JSQLParserException { + String sqlStr = "CREATE TRIGGER trg BEFORE INSERT ON MY_TABLE1 FOR EACH ROW " + + "SET NEW.a = (SELECT MAX(id) FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + + sqlStr = "CREATE TRIGGER trg BEFORE UPDATE ON MY_TABLE1 FOR EACH ROW " + + "BEGIN SET NEW.updated_at = NOW(); INSERT INTO MY_TABLE2 VALUES (NEW.id); END"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); } @Test @@ -532,13 +561,13 @@ void testSubqueryAliasesIssue1987() throws JSQLParserException { assertThat(tables).containsExactlyInAnyOrder("a", "b"); assertThat(tables).doesNotContain("a1"); + // a sub-select cannot select from an alias: this statement only runs if a1 is a table sqlStr = "SELECT * FROM b, (SELECT * FROM a) as a1 WHERE b.id IN ( SELECT id FROM a1 )"; tables = TablesNamesFinder.findTablesOrOtherSources(sqlStr); assertThat(tables).containsExactlyInAnyOrder("a1", "a", "b"); tables = TablesNamesFinder.findTables(sqlStr); - assertThat(tables).containsExactlyInAnyOrder("a", "b"); - assertThat(tables).doesNotContain("a1"); + assertThat(tables).containsExactlyInAnyOrder("a", "a1", "b"); sqlStr = "select (a_alias.col1), b_alias.col2\n" + "from b b_alias, a as a_alias, c join b on c.id = b.id\n" + @@ -562,14 +591,14 @@ void testSubqueryAliasesIssue1987() throws JSQLParserException { + " and a.max > (select max(val) from bc_nested, f) and a.desc like 'abc'"; tables = TablesNamesFinder.findTables(sqlStr); - assertThat(tables).containsExactlyInAnyOrder("a", "b", "c", "d", "e", "f"); + assertThat(tables).containsExactlyInAnyOrder("a", "b", "bc_nested", "c", "d", "e", "f"); sqlStr = " select (select max(val) from e) as maxval, id\n" + " from (select * from b, (select * from c) c_alias) as bc_nested, a\n" + " where a.max > (select max(val) from bc_nested, f) and \n" + " a.id in ( select id from (select * from d) d_alias join bc_nested on bc_nested.id = d_alias.id )"; tables = TablesNamesFinder.findTables(sqlStr); - assertThat(tables).containsExactlyInAnyOrder("a", "b", "c", "d", "e", "f"); + assertThat(tables).containsExactlyInAnyOrder("a", "b", "bc_nested", "c", "d", "e", "f"); sqlStr = "select a.id, bc_nested.id\n" + " from (select * from b, (select * from c) c_alias) as bc_nested, a\n" + @@ -581,7 +610,7 @@ void testSubqueryAliasesIssue1987() throws JSQLParserException { " on bc_nested_alias.id = d.id\n" + " )))"; tables = TablesNamesFinder.findTables(sqlStr); - assertThat(tables).containsExactlyInAnyOrder("a", "b", "c", "d", "e"); + assertThat(tables).containsExactlyInAnyOrder("a", "b", "bc_nested", "c", "d", "e"); sqlStr = "select id\n" + "from (select * from c, (select * from b) b_alias) as bc_nested, a\n" + @@ -591,7 +620,7 @@ void testSubqueryAliasesIssue1987() throws JSQLParserException { "join bc_nested on bc_nested_alias.id = bc_nested.id\n" + ")"; tables = TablesNamesFinder.findTables(sqlStr); - assertThat(tables).containsExactlyInAnyOrder("a", "b", "c", "d", "e"); + assertThat(tables).containsExactlyInAnyOrder("a", "b", "bc_nested", "c", "d", "e"); sqlStr = "with\n" + " temp1 as (\n" + @@ -607,7 +636,7 @@ void testSubqueryAliasesIssue1987() throws JSQLParserException { + "where exist ( select * from e, e_alias where e.test = dtest.test ) and temp1.max = (select max(column_1) from g)"; tables = TablesNamesFinder.findTables(sqlStr); - assertThat(tables).containsExactlyInAnyOrder("a", "b", "c", "d", "e", "f", "g"); + assertThat(tables).containsExactlyInAnyOrder("a", "b", "c", "d", "e", "e_alias", "f", "g"); } @Test @@ -619,7 +648,110 @@ void testSubqueryAliasesIssue2035() throws JSQLParserException { assertThat(tables).containsExactlyInAnyOrder("A", "B", "C"); tables = TablesNamesFinder.findTables(sqlStr); - assertThat(tables).containsExactlyInAnyOrder("B", "C"); + assertThat(tables).containsExactlyInAnyOrder("A", "B", "C"); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * FROM (SELECT * FROM secrets) AS secrets", + "SELECT (SELECT max(pw) FROM secrets) FROM (SELECT 1) AS secrets", + "SELECT * FROM (SELECT 1) AS secrets, LATERAL (SELECT * FROM secrets) AS x", + "SELECT * FROM t, LATERAL (SELECT * FROM secrets) AS secrets", + "FROM secrets |> AS secrets", + "DELETE FROM secrets WHERE id IN (SELECT id FROM (SELECT 1 AS id) AS secrets)", + "UPDATE secrets SET pw = 'x' WHERE id IN (SELECT id FROM (SELECT 1 AS id) AS secrets)", + "INSERT INTO secrets (id) SELECT id FROM (SELECT 1 AS id) AS secrets", + "MERGE INTO secrets AS t USING (SELECT 1 AS id) AS secrets ON (t.id = secrets.id)" + + " WHEN MATCHED THEN DELETE"}) + void testAliasDoesNotHideTable(String sqlStr) throws JSQLParserException { + assertThat(TablesNamesFinder.findTables(sqlStr)).contains("secrets"); + } + + @ParameterizedTest + @ValueSource(strings = { + // without RECURSIVE a WITH item does not see itself or the items after it + "WITH secrets AS (SELECT * FROM secrets) SELECT * FROM secrets", + "WITH secrets AS (SELECT 1 AS id UNION ALL SELECT id FROM secrets) SELECT * FROM secrets", + "WITH x AS (SELECT id FROM secrets), secrets AS (SELECT 1 AS id) SELECT * FROM x, secrets", + // a WITH item only exists in the query that declares it + "SELECT * FROM secrets WHERE EXISTS (WITH secrets AS (SELECT 1 AS a) SELECT a FROM secrets)", + "SELECT id FROM secrets UNION ALL (WITH secrets AS (SELECT 1 AS id) SELECT id FROM secrets)", + "SELECT * FROM (WITH secrets AS (SELECT 1 AS id) SELECT * FROM secrets) AS s, secrets", + "WITH a AS (WITH secrets AS (SELECT 1 AS id) SELECT * FROM secrets) SELECT * FROM a, secrets", + // the target of a DML statement is never a WITH item + "WITH secrets AS (SELECT 1 AS id) DELETE FROM secrets", + "WITH secrets AS (SELECT 1 AS id) UPDATE secrets SET pw = 'x'", + "WITH secrets AS (SELECT 1 AS id) INSERT INTO secrets (id) SELECT id FROM secrets", + "WITH secrets AS (SELECT 1 AS id) MERGE INTO secrets USING secrets AS s" + + " ON (s.id = secrets.id) WHEN MATCHED THEN DELETE", + "WITH secrets AS (SELECT 1 AS id), d AS (DELETE FROM secrets RETURNING *) SELECT * FROM d", + "WITH secrets AS (SELECT 1 AS id), d AS (UPDATE secrets SET pw = 'x' RETURNING *)" + + " SELECT * FROM d", + "WITH secrets AS (SELECT 1 AS id), d AS (INSERT INTO secrets (id) VALUES (2) RETURNING *)" + + " SELECT * FROM d", + // WITH on the kinds of select that used to skip it + "SELECT * FROM (WITH a AS (SELECT * FROM secrets) TABLE a) AS s", + "SELECT * FROM (WITH a AS (SELECT id FROM secrets) VALUES ((SELECT max(id) FROM a))) AS s"}) + void testWithItemDoesNotHideTableOutsideItsScope(String sqlStr) throws JSQLParserException { + assertThat(TablesNamesFinder.findTables(sqlStr)).contains("secrets"); + } + + @ParameterizedTest + @ValueSource(strings = { + "WITH RECURSIVE t(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM t WHERE n < 5) SELECT * FROM t", + // RECURSIVE is parsed onto the first item, but covers the whole list + "WITH RECURSIVE a AS (SELECT 1 AS n), t(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM t" + + " WHERE n < 5) SELECT * FROM t, a", + "WITH RECURSIVE x AS (SELECT id FROM y), y AS (SELECT 1 AS id) SELECT * FROM x", + "WITH a AS (SELECT 1 AS id), b AS (SELECT * FROM a) SELECT * FROM b", + "WITH a AS (SELECT 1 AS id) SELECT id FROM a UNION SELECT id FROM a", + "WITH a AS (SELECT 1 AS id) SELECT * FROM a WHERE id IN (SELECT id FROM a)", + "WITH a AS (SELECT 1 AS id) (SELECT * FROM a)", + "WITH a AS (SELECT 1 AS id) SELECT * FROM (SELECT * FROM a) AS s", + // a nested WITH repeating a name does not end the enclosing one + "WITH a AS (SELECT 1 AS id) SELECT * FROM (WITH a AS (SELECT 2 AS id) SELECT * FROM a) AS s, a", + "SELECT * FROM (WITH a AS (SELECT 1 AS id) TABLE a) AS s", + "SELECT * FROM (WITH a AS (SELECT 1 AS id) VALUES ((SELECT max(id) FROM a))) AS s"}) + void testWithItemInScopeIsNoTable(String sqlStr) throws JSQLParserException { + assertThat(TablesNamesFinder.findTables(sqlStr)).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = { + "WITH a AS (SELECT 1 AS id) INSERT INTO t (id) SELECT id FROM a", + "INSERT INTO t (id) WITH a AS (SELECT 1 AS id) SELECT id FROM a", + "WITH a AS (SELECT 1 AS id) UPDATE t SET id = a.id FROM a", + "WITH a AS (SELECT 1 AS id) DELETE FROM t USING a WHERE t.id = a.id", + "WITH a AS (SELECT 1 AS id) MERGE INTO t USING a ON (t.id = a.id) WHEN MATCHED THEN DELETE"}) + void testWithItemInScopeOfDmlIsNoTable(String sqlStr) throws JSQLParserException { + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactly("t"); + } + + @Test + void testWithItemDoesNotHideQualifiedTable() throws JSQLParserException { + String sqlStr = "WITH secrets AS (SELECT 1 AS id) SELECT * FROM public.secrets"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactly("public.secrets"); + + TablesNamesFinder finderWithoutSchema = new TablesNamesFinder() { + @Override + protected String extractTableName(Table table) { + return table.getName(); + } + }; + assertThat(finderWithoutSchema.getTables(CCJSqlParserUtil.parse(sqlStr))) + .containsExactly("secrets"); + } + + @Test + void testAliasInExpression() throws JSQLParserException { + assertThat(TablesNamesFinder + .findTablesInExpression("(SELECT 1 FROM (SELECT * FROM secrets) AS secrets)")) + .containsExactly("secrets"); + + // an alias still is no table when it qualifies a column + assertThat(TablesNamesFinder + .findTablesInExpression("(SELECT max(id) FROM (SELECT id FROM t) AS x) = x.id")) + .containsExactly("t"); } @Test @@ -773,10 +905,326 @@ void testJsonTable() throws JSQLParserException { @Test void testWindowExpressionWithNoRangeAndNoOffsetDoesNotThrowException() { - String sqlStr = "SELECT c, SUM(COUNT(*)) OVER (ORDER BY c ASC ROWS UNBOUNDED PRECEDING) FROM tbl GROUP BY c"; + String sqlStr = + "SELECT c, SUM(COUNT(*)) OVER (ORDER BY c ASC ROWS UNBOUNDED PRECEDING) FROM tbl GROUP BY c"; assertThatCode(() -> TablesNamesFinder.findTables(sqlStr)) .doesNotThrowAnyException(); } + @Test + void testPipedQuery() throws JSQLParserException { + String sqlStr = "FROM MY_TABLE1\n" + + "|> WHERE id IN (SELECT id FROM MY_TABLE2)\n" + + "|> LEFT JOIN (SELECT item, id FROM MY_TABLE3) AS t3 ON t3.item = item\n" + + "|> UNION ALL (SELECT * FROM MY_TABLE4)\n" + + "|> SELECT item"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3", "MY_TABLE4"); + } + + @Test + void testDeleteWithWithItemList() throws JSQLParserException { + String sqlStr = + "WITH cte AS (SELECT * FROM MY_TABLE2) DELETE FROM MY_TABLE1 WHERE id IN (SELECT id FROM cte)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testMergeOnConditionAndOperations() throws JSQLParserException { + String sqlStr = "MERGE INTO MY_TABLE1 t USING MY_TABLE2 s " + + "ON t.id IN (SELECT id FROM MY_TABLE3) " + + "WHEN MATCHED AND t.v > (SELECT MIN(v) FROM MY_TABLE4) THEN UPDATE SET t.v = (SELECT MAX(v) FROM MY_TABLE5) " + + "WHEN NOT MATCHED THEN INSERT (v) VALUES (1)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3", "MY_TABLE4", "MY_TABLE5"); + } + + @Test + void testInsertWithSetUpdateSets() throws JSQLParserException { + String sqlStr = "INSERT INTO MY_TABLE1 SET a = (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testInsertWithOnDuplicateKeyUpdate() throws JSQLParserException { + String sqlStr = + "INSERT INTO MY_TABLE1 (a) VALUES (1) ON DUPLICATE KEY UPDATE b = (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testInsertWithOnConflictDoUpdate() throws JSQLParserException { + String sqlStr = + "INSERT INTO MY_TABLE1 (a) VALUES (1) ON CONFLICT (a) DO UPDATE SET b = (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testWithDataModifyingCte() throws JSQLParserException { + String sqlStr = + "WITH del AS (DELETE FROM MY_TABLE2) INSERT INTO MY_TABLE1 SELECT * FROM del"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testUpdateWithOutputClause() throws JSQLParserException { + String sqlStr = + "UPDATE MY_TABLE1 SET a = 1 OUTPUT (SELECT x FROM MY_TABLE2) INTO @tv"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testUpdateWithReturningClause() throws JSQLParserException { + String sqlStr = "UPDATE MY_TABLE1 SET a = 1 RETURNING (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testInsertWithReturningClause() throws JSQLParserException { + String sqlStr = + "INSERT INTO MY_TABLE1 (a) VALUES (1) RETURNING (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testDeleteWithReturningClause() throws JSQLParserException { + String sqlStr = "DELETE FROM MY_TABLE1 RETURNING (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testAnalyticFunctionsWithFunctionOrderBy() throws JSQLParserException { + String sqlStr = + "SELECT string_agg(name, ',' ORDER BY id) OVER (PARTITION BY grp) FROM MY_TABLE1"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1"); + } + + @Test + void testAnalyticFunctionsWithWindowOrderBy() throws JSQLParserException { + String sqlStr = + "SELECT SUM(v) OVER (ORDER BY (SELECT MAX(k) FROM MY_TABLE2)) FROM MY_TABLE1"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testAnalyticFunctionsWithFilterClause() throws JSQLParserException { + String sqlStr = + "SELECT SUM(v) FILTER (WHERE id IN (SELECT id FROM MY_TABLE2)) OVER () FROM MY_TABLE1"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectIntoTables() throws JSQLParserException { + String sqlStr = "SELECT * INTO MY_TABLE1 FROM MY_TABLE2"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectIntoTempTable() throws JSQLParserException { + String sqlStr = "SELECT * FROM MY_TABLE2 INTO TEMP MY_TABLE1"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testGroupBySubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 GROUP BY A, (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testGroupByGroupingSetsSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 GROUP BY GROUPING SETS ((A, (SELECT B FROM MY_TABLE2)), (C))"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectQualifySubquery() throws JSQLParserException { + String sqlStr = "SELECT * FROM MY_TABLE1 QUALIFY (SELECT B FROM MY_TABLE2) = 1"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testWindowDefinitionSubquery() throws JSQLParserException { + String sqlStr = + "SELECT * FROM MY_TABLE1 WINDOW W AS (PARTITION BY (SELECT B FROM MY_TABLE2))"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectOrderBySubquery() throws JSQLParserException { + String sqlStr = "SELECT A FROM MY_TABLE1 ORDER BY A, (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectLimitSubquery() throws JSQLParserException { + String sqlStr = "SELECT A FROM MY_TABLE1 LIMIT (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectLimitBySubquery() throws JSQLParserException { + String sqlStr = "SELECT A FROM MY_TABLE1 LIMIT 2 BY (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectOffsetSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 LIMIT 1 OFFSET (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectFetchSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 OFFSET 1 ROWS FETCH NEXT (SELECT B FROM MY_TABLE2) ROWS ONLY"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectDistinctOnSubquery() throws JSQLParserException { + String sqlStr = "SELECT DISTINCT ON ((SELECT B FROM MY_TABLE2)) A FROM MY_TABLE1"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testLateralViewSubquery() throws JSQLParserException { + String sqlStr = + "SELECT * FROM MY_TABLE1 LATERAL VIEW EXPLODE(ARRAY((SELECT B FROM MY_TABLE2))) V AS X"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testTablePivotXmlSubquery() throws JSQLParserException { + String sqlStr = + "SELECT * FROM MY_TABLE1 PIVOT XML (SUM(X) FOR Y IN (SELECT Z FROM MY_TABLE2))"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testTablePivotSimple() throws JSQLParserException { + String sqlStr = "SELECT * FROM MY_TABLE1 PIVOT (SUM(X) FOR Y IN ('a', 'b'))"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1"); + } + + @Test + void testParenthesedOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "(SELECT A FROM MY_TABLE1) ORDER BY (SELECT B FROM MY_TABLE2) LIMIT (SELECT C FROM MY_TABLE3)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3"); + } + + @Test + void testSetOperationOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 UNION SELECT B FROM MY_TABLE2 ORDER BY (SELECT C FROM MY_TABLE3) LIMIT (SELECT D FROM MY_TABLE4)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3", "MY_TABLE4"); + } + + @Test + void testSelectSettingsSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 SETTINGS X = (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testValuesOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "VALUES (1), (2) ORDER BY (SELECT A FROM MY_TABLE1) LIMIT (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testDeleteOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "DELETE FROM MY_TABLE1 WHERE A = 1 ORDER BY (SELECT B FROM MY_TABLE2) LIMIT 2"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testUpdateOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "UPDATE MY_TABLE1 SET A = 1 ORDER BY (SELECT B FROM MY_TABLE2) LIMIT 2"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testManyDerivedNamesDoNotScaleQuadratically() { + // Every plain table reference used to be matched against the CTE aliases with + // ArrayList.contains, so n aliases and n tables cost O(n^2) to walk. + final int n = 30000; + final PlainSelect statement = new PlainSelect() + .withSelectItems(new SelectItem<>(new AllColumns())) + .withFromItem(new Table("t0")); + final List> withItems = new ArrayList<>(n); + final List joins = new ArrayList<>(n - 1); + for (int i = 0; i < n; i++) { + PlainSelect body = + new PlainSelect().withSelectItems(new SelectItem<>(new LongValue(1))); + withItems.add(new WithItem<>(new ParenthesedSelect().withSelect(body), + new Alias("a" + i))); + if (i > 0) { + joins.add(new Join().withSimple(true).withRightItem(new Table("t" + i))); + } + } + statement.setWithItemsList(withItems); + statement.setJoins(joins); + + // Build the AST directly: this tests table traversal, and parsing a large SQL + // fixture can hit the parser's independent timeout before traversal even starts. + final Set tables = assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> new TablesNamesFinder<>().getTables((Statement) statement)); + assertEquals(n, tables.size()); + } + } diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTraversalTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTraversalTest.java new file mode 100644 index 0000000000..38925bb3de --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTraversalTest.java @@ -0,0 +1,363 @@ +/*- + * #%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.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ArrayExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class TablesNamesFinderTraversalTest { + private static final String SUBQUERY = "(SELECT max(x) FROM t2)"; + + @ParameterizedTest + @MethodSource("nestedTables") + void findsTablesInNestedChildren(String sql, List expected) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertThat(TablesNamesFinder.findTables(sql)).containsExactlyInAnyOrderElementsOf(expected); + assertThat(new TablesNamesFinder<>().getTables(statement)) + .containsExactlyInAnyOrderElementsOf(expected); + } + + private static Stream nestedTables() { + return Stream.of( + tables("SELECT a[(SELECT max(x) FROM t2)] FROM t1", "t1", "t2"), + tables("SELECT a[1][(SELECT max(x) FROM t2)] FROM t1", "t1", "t2"), + tables("SELECT f()[(SELECT max(x) FROM t2)] FROM t1", "t1", "t2"), + tables("SELECT f((SELECT x FROM t2))[(SELECT y FROM t3)] FROM t1", "t1", "t2", + "t3"), + tables("SELECT a[OFFSET(1):2] FROM t1", "t1"), + tables("SELECT f()[OFFSET(1):OFFSET(2)] FROM t1", "t1"), + tables("SELECT f()[1:] FROM t1", "t1"), + tables("SELECT f()[:2] FROM t1", "t1"), + tables("SELECT f()[:] FROM t1", "t1"), + tables("SELECT f()[OFFSET((SELECT x FROM t2)):OFFSET((SELECT y FROM t3))] FROM t1", + "t1", "t2", + "t3"), + tables("FROM t1 JOIN t2 ON t1.id=t2.id |> SELECT t1.id", "t1", "t2"), + tables("FROM t1 JOIN t2 ON t1.id=(SELECT id FROM t3) |> SELECT t1.id", "t1", "t2", + "t3"), + tables("FROM t1 LEFT JOIN (SELECT id FROM t2) q ON t1.id=q.id |> SELECT t1.id", + "t1", "t2"), + tables("FROM t1 LATERAL VIEW explode((SELECT a FROM t2)) e AS x |> SELECT x", "t1", + "t2"), + tables("WITH c AS (SELECT a FROM t2) FROM t1 JOIN c ON t1.a=c.a |> SELECT t1.a", + "t1", "t2"), + tables("UPDATE t1 SET a=1 OUTPUT inserted.* INTO t2", "t1", "t2"), + tables("INSERT INTO t1 OUTPUT inserted.* INTO t2 VALUES (1)", "t1", "t2"), + tables("DELETE t1 OUTPUT deleted.* INTO t2 FROM t1", "t1", "t2"), + tables("MERGE INTO t1 USING t2 ON t1.a=t2.a WHEN MATCHED THEN DELETE OUTPUT deleted.* INTO t3", + "t1", "t2", "t3"), + tables("UPDATE t1 SET a=1 OUTPUT (SELECT x FROM t2) INTO t3", "t1", "t2", "t3"), + tables("MERGE INTO t1 USING t2 ON t1.a=t2.a WHEN MATCHED THEN DELETE OUTPUT (SELECT x FROM t3) INTO t4", + "t1", "t2", "t3", "t4"), + tables("MERGE INTO t1 USING t2 ON t1.a=t2.a WHEN MATCHED THEN DELETE OUTPUT deleted.* INTO @tv", + "t1", "t2"), + tables("UPSERT INTO t1 VALUES (1) ON DUPLICATE KEY UPDATE (a)=(SELECT max(x) FROM t2)", + "t1", "t2"), + tables("UPSERT INTO t1 SELECT a FROM t2 ON DUPLICATE KEY UPDATE b=(SELECT x FROM t3)", + "t1", "t2", "t3"), + tables("SELECT POSITION('x' IN (SELECT max(a) FROM t2)) FROM t1", "t1", "t2"), + tables("SELECT SUBSTRING((SELECT a FROM t2) FROM (SELECT n FROM t3) FOR (SELECT n FROM t4)) FROM t1", + "t1", "t2", "t3", "t4"), + tables("IF (SELECT count(*) FROM t2)>1 DELETE FROM t3", "t2", "t3"), + tables("IF EXISTS (SELECT 1 FROM t1) UPDATE t2 SET a=1 ELSE DELETE FROM t3", "t1", + "t2", "t3"), + tables("SELECT sum(x) OVER (PARTITION BY (SELECT a FROM t2)) FROM t1", "t1", "t2"), + tables("SELECT sum(x) OVER (PARTITION BY (SELECT a FROM t2) ORDER BY (SELECT b FROM t3)) FROM t1", + "t1", "t2", "t3"), + tables("SELECT a[1], f()[1] FROM t1", "t1"), + tables("SELECT a FROM t1 JOIN t2 ON t1.a=t2.a", "t1", "t2"), + tables("SELECT * FROM t1 LATERAL VIEW explode((SELECT a FROM t2)) e AS x", "t1", + "t2"), + tables("FROM t1 |> JOIN t2 ON t1.a=t2.a |> SELECT t1.a", "t1", "t2"), + tables("UPDATE t1 SET a=1 OUTPUT inserted.*, deleted.a INTO @tv", "t1"), + tables("UPDATE t1 SET a=1 OUTPUT inserted.*", "t1"), + tables("INSERT INTO t1 VALUES (1) ON DUPLICATE KEY UPDATE a=(SELECT x FROM t2)", + "t1", "t2"), + tables("UPSERT INTO t1 VALUES (1)", "t1"), + tables("SELECT POSITION('x' IN 'xx') FROM t1", "t1"), + tables("IF a>1 DELETE FROM t3", "t3"), + tables("SELECT sum(x) OVER (ORDER BY (SELECT a FROM t2)) FROM t1", "t1", "t2"), + tables("SELECT max(a) KEEP (DENSE_RANK FIRST ORDER BY (SELECT x FROM t2)) FROM t1", + "t1", "t2"), + tables("SELECT max(a) KEEP (DENSE_RANK FIRST ORDER BY (SELECT x FROM t2)) OVER (PARTITION BY b) FROM t1", + "t1", "t2"), + tables("SELECT array_agg(a LIMIT (SELECT x FROM t2)) FROM t1", "t1", "t2"), + tables("SELECT any_value(a HAVING MAX (SELECT x FROM t2)) FROM t1", "t1", "t2"), + tables("SELECT any_value(a HAVING MAX (SELECT x FROM t2)) OVER () FROM t1", "t1", + "t2"), + tables("SELECT group_concat(a SEPARATOR (SELECT x FROM t2)) FROM t1", "t1", "t2"), + tables("SELECT group_concat(a ORDER BY (SELECT x FROM t2)) FROM t1", "t1", "t2"), + tables("SELECT f(a).g((SELECT x FROM t2)) FROM t1", "t1", "t2"), + tables("SELECT a FROM t1 WHERE (SELECT x FROM t2) IS UNKNOWN", "t1", "t2"), + tables("SELECT a FROM t1 WHERE MATCH (a) AGAINST ((SELECT x FROM t2))", "t1", + "t2"), + tables("SELECT JSON_OBJECTAGG(KEY a VALUE (SELECT x FROM t2)) FROM t1", "t1", + "t2"), + tables("SELECT JSON_ARRAYAGG(a ORDER BY (SELECT x FROM t2)) FROM t1", "t1", "t2"), + tables("SELECT JSON_ARRAYAGG(a) OVER (PARTITION BY (SELECT x FROM t2) ORDER BY (SELECT y FROM t3)) FROM t1", + "t1", "t2", "t3"), + tables("SELECT JSON_ARRAYAGG(a) OVER (ORDER BY b ROWS (SELECT x FROM t2) PRECEDING) FROM t1", + "t1", "t2"), + tables("SELECT * FROM XMLTABLE('/r' PASSING (SELECT x FROM t2) COLUMNS a int PATH 'a') x", + "t2"), + tables("SELECT * FROM JSON_TABLE((SELECT x FROM t2), '$' COLUMNS (a int PATH '$.a')) j", + "t2"), + tables("SELECT * FROM generate_series(1, (SELECT max(x) FROM t2)) g", "t2")); + } + + @ParameterizedTest + @MethodSource("postgresTables") + void findsTablesInPostgresExpressionChildren(String sql, List expected) + throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertThat(new TablesNamesFinder<>().getTables(statement)) + .containsExactlyInAnyOrderElementsOf(expected); + assertThat(new TablesNamesFinder<>().getTablesOrOtherSources(statement)) + .containsAll(expected); + } + + private static Stream postgresTables() { + return Stream.of( + // aggregate ORDER BY inside the call + tables("SELECT string_agg(a, ',' ORDER BY " + SUBQUERY + ") FROM t1", "t1", + "t2"), + tables("SELECT array_agg(a ORDER BY " + SUBQUERY + ") FROM t1", "t1", + "t2"), + // LIKE ... ESCAPE + tables("SELECT a FROM t1 WHERE a LIKE 'a' ESCAPE " + SUBQUERY, "t1", + "t2"), + // AT TIME ZONE, zone operand + tables("SELECT now() AT TIME ZONE " + SUBQUERY, "t2"), + // JSON operators, right operand + tables("SELECT j -> " + SUBQUERY + " FROM t1", "t1", "t2"), + tables("SELECT j ->> " + SUBQUERY + " FROM t1", "t1", "t2"), + tables("SELECT j #> ARRAY[" + SUBQUERY + "] FROM t1", "t1", "t2"), + tables("SELECT j #>> ARRAY[" + SUBQUERY + "] FROM t1", "t1", "t2"), + // array slice bounds and subscript + tables("SELECT a[1:(SELECT count(*)::int FROM t2)] FROM t1", "t1", + "t2"), + tables("SELECT a[(SELECT count(*)::int FROM t2):2] FROM t1", "t1", + "t2"), + tables("SELECT a[(SELECT count(*)::int FROM t2)] FROM t1", "t1", + "t2"), + // positions that were already traversed + tables("SELECT f(" + SUBQUERY + ") FROM t1", "t1", "t2"), + tables("SELECT count(*) FILTER (WHERE a = " + SUBQUERY + ") FROM t1", "t1", + "t2"), + tables("SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY " + SUBQUERY + + ") FROM t1", "t1", "t2"), + tables("SELECT sum(a) OVER (PARTITION BY " + SUBQUERY + ") FROM t1", "t1", + "t2"), + tables("SELECT sum(a) OVER (ORDER BY " + SUBQUERY + ") FROM t1", "t1", + "t2"), + tables("SELECT sum(a) OVER (ORDER BY b ROWS BETWEEN " + SUBQUERY + + " PRECEDING AND CURRENT ROW) FROM t1", "t1", "t2"), + tables("SELECT a FROM t1 LIMIT " + SUBQUERY, "t1", "t2"), + tables("SELECT a FROM t1 OFFSET " + SUBQUERY, "t1", "t2"), + tables("SELECT a FROM t1 FETCH FIRST " + SUBQUERY + " ROWS ONLY", "t1", + "t2"), + tables("SELECT a FROM t1 WHERE a = ANY (SELECT x FROM t2)", "t1", + "t2"), + tables("SELECT a FROM t1 WHERE a > ALL (SELECT x FROM t2)", "t1", + "t2"), + tables("SELECT a FROM t1 WHERE a = SOME (SELECT x FROM t2)", "t1", + "t2"), + tables("SELECT ARRAY(SELECT x FROM t2)", "t2"), + tables("SELECT * FROM (VALUES (" + SUBQUERY + ")) v(x)", "t2"), + tables("SELECT substring(a FROM " + SUBQUERY + ") FROM t1", "t1", "t2"), + tables("SELECT trim(BOTH " + SUBQUERY + " FROM a) FROM t1", "t1", "t2"), + tables("SELECT position(" + SUBQUERY + " IN a) FROM t1", "t1", "t2"), + tables("SELECT overlay(a PLACING " + SUBQUERY + " FROM 1) FROM t1", "t1", + "t2"), + tables("SELECT CASE WHEN a = 1 THEN " + SUBQUERY + " END FROM t1", "t1", + "t2"), + tables("SELECT CAST(" + SUBQUERY + " AS text) FROM t1", "t1", "t2"), + tables("SELECT " + SUBQUERY + "::text FROM t1", "t1", "t2"), + tables("SELECT count(*) FROM t1 GROUP BY " + SUBQUERY, "t1", "t2"), + tables("SELECT count(*) FROM t1 GROUP BY GROUPING SETS ((" + SUBQUERY + "))", "t1", + "t2"), + tables("SELECT DISTINCT ON (" + SUBQUERY + ") a FROM t1", "t1", "t2"), + tables("SELECT * FROM t1, LATERAL (SELECT x FROM t2) p", "t1", + "t2")); + } + + private static Arguments tables(String sql, String... names) { + return Arguments.of(sql, List.of(names)); + } + + @ParameterizedTest + @MethodSource("expressionTables") + void findsTablesThroughExpressionEntryPoints(String sql, List expected) + throws JSQLParserException { + Expression expression = CCJSqlParserUtil.parseExpression(sql); + assertThat(TablesNamesFinder.findTablesInExpression(sql)) + .containsExactlyInAnyOrderElementsOf(expected); + assertThat(new TablesNamesFinder<>().getTables(expression)) + .containsExactlyInAnyOrderElementsOf(expected); + } + + private static Stream expressionTables() { + return Stream.of(tables("a[(SELECT x FROM t2)]", "t2"), + tables("f()[(SELECT x FROM t2)]", "t2"), + tables("f()[OFFSET((SELECT x FROM t2)):OFFSET((SELECT y FROM t3))]", "t2", "t3"), + tables("POSITION('x' IN (SELECT a FROM t2))", "t2"), + tables("sum(x) OVER (PARTITION BY (SELECT a FROM t2))", "t2"), + tables("string_agg(a, ',' ORDER BY (SELECT x FROM t2))", "t2"), + tables("a LIKE 'a' ESCAPE (SELECT x FROM t2)", "t2"), + tables("now() AT TIME ZONE (SELECT x FROM t2)", "t2"), + tables("(SELECT x FROM t2) IS UNKNOWN", "t2"), + tables("a[1]"), tables("f()[1]"), tables("f()[:2]"), tables("f()[:]"), + tables("f(a).b.c")); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7}) + void visitsArrayOperandsOnce(int fields) throws JSQLParserException { + Expression index = (fields & 1) == 0 ? null + : CCJSqlParserUtil.parseExpression("(SELECT x FROM index_table)"); + Expression start = (fields & 2) == 0 ? null + : CCJSqlParserUtil.parseExpression("(SELECT x FROM start_table)"); + Expression stop = (fields & 4) == 0 ? null + : CCJSqlParserUtil.parseExpression("(SELECT x FROM stop_table)"); + ArrayExpression expression = new ArrayExpression( + CCJSqlParserUtil.parseExpression("f((SELECT x FROM object_table))"), index, start, + stop); + Map expected = new HashMap<>(); + expected.put("object_table", 1); + if (index != null) { + expected.put("index_table", 1); + } + if (start != null) { + expected.put("start_table", 1); + } + if (stop != null) { + expected.put("stop_table", 1); + } + CountingFinder finder = new CountingFinder(); + assertThat(finder.getTables(expression)) + .containsExactlyInAnyOrderElementsOf(expected.keySet()); + assertThat(finder.visits).isEqualTo(expected); + } + + @Test + void visitsNewChildrenWithoutRepeatingExistingChildren() throws JSQLParserException { + CountingFinder finder = new CountingFinder(); + Statement statement = CCJSqlParserUtil.parse( + "FROM t1 JOIN t2 ON t1.a=(SELECT a FROM t3) |> SELECT sum(a) OVER (PARTITION BY (SELECT a FROM t4) ORDER BY (SELECT a FROM t5))"); + assertThat(finder.getTables(statement)).containsExactlyInAnyOrder("t1", "t2", "t3", "t4", + "t5"); + assertThat(finder.visits).isEqualTo(Map.of("t1", 1, "t2", 1, "t3", 1, "t4", 1, "t5", 1)); + } + + @Test + void visitsAggregateChildrenOnce() throws JSQLParserException { + CountingFinder finder = new CountingFinder(); + Statement statement = CCJSqlParserUtil.parse( + "SELECT max(a) KEEP (DENSE_RANK FIRST ORDER BY (SELECT x FROM t2)) OVER (PARTITION BY (SELECT y FROM t3)), string_agg(a, ',' ORDER BY (SELECT z FROM t4)) FROM t1"); + assertThat(finder.getTables(statement)).containsExactlyInAnyOrder("t1", "t2", "t3", + "t4"); + assertThat(finder.visits).isEqualTo(Map.of("t1", 1, "t2", 1, "t3", 1, "t4", 1)); + } + + @Test + void resetsStateBetweenStatementAndExpressionVisits() throws JSQLParserException { + TablesNamesFinder finder = new TablesNamesFinder<>(); + assertThat(finder.getTables(CCJSqlParserUtil.parse("SELECT a FROM t1"))) + .containsExactlyInAnyOrder("t1"); + assertThat(finder.getTables(CCJSqlParserUtil.parseExpression("qualified.a"))) + .containsExactlyInAnyOrder("qualified"); + assertThat(finder + .getTables(CCJSqlParserUtil.parse("UPDATE t3 SET a=1 OUTPUT inserted.* INTO @tv"))) + .containsExactlyInAnyOrder("t3"); + assertThat(finder.getTables(CCJSqlParserUtil.parse("SELECT a FROM t4"))) + .containsExactlyInAnyOrder("t4"); + } + + @Test + void visitsParsedSliceBounds() throws JSQLParserException { + String sql = "f()[OFFSET((SELECT x FROM t1)):OFFSET((SELECT y FROM t2))]"; + Expression expression = CCJSqlParserUtil.parseExpression(sql); + assertThat(expression).isInstanceOf(ArrayExpression.class); + ArrayExpression array = (ArrayExpression) expression; + assertThat(array.getIndexExpression()).isNull(); + assertThat(array.getStartIndexExpression()).isNotNull(); + assertThat(array.getStopIndexExpression()).isNotNull(); + assertThat(new TablesNamesFinder<>().getTables(array)).containsExactlyInAnyOrder("t1", + "t2"); + } + + @Test + void traversesSliceBounds() throws JSQLParserException { + Expression expression = CCJSqlParserUtil.parseExpression("f()[1:2]"); + assertThat(expression).isInstanceOf(ArrayExpression.class); + ArrayExpression slice = (ArrayExpression) expression; + assertThat(slice.getIndexExpression()).isNull(); + assertThat(slice.getStartIndexExpression()).hasToString("1"); + assertThat(slice.getStopIndexExpression()).hasToString("2"); + assertThat(new TablesNamesFinder<>().getTables(expression)).isEmpty(); + } + + @Test + void findsTablesInUpsertActionCondition() throws JSQLParserException { + Upsert upsert = (Upsert) CCJSqlParserUtil + .parse("UPSERT INTO t1 VALUES (1) ON DUPLICATE KEY UPDATE a=(SELECT x FROM t2)"); + upsert.getDuplicateAction().setWhereExpression( + CCJSqlParserUtil.parseCondExpression("EXISTS (SELECT 1 FROM t3)")); + assertThat(new TablesNamesFinder<>().getTables(upsert)).containsExactlyInAnyOrder("t1", + "t2", "t3"); + } + + @Test + void preservesCteNamesInOtherSourcesEntryPoints() throws JSQLParserException { + String sql = "WITH c AS (SELECT a FROM t2) FROM t1 JOIN c ON t1.a=c.a |> SELECT t1.a"; + Statement statement = CCJSqlParserUtil.parse(sql); + assertThat(TablesNamesFinder.findTablesOrOtherSources(sql)).containsExactlyInAnyOrder("t1", + "t2", "c"); + assertThat(new TablesNamesFinder<>().getTablesOrOtherSources(statement)) + .containsExactlyInAnyOrder("t1", "t2", "c"); + } + + @Test + void preservesParseAndDeparse() throws JSQLParserException { + String sql = "SELECT a[(SELECT max(x) FROM t2)] FROM t1"; + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + assertThat(new TablesNamesFinder<>().getTables(statement)).containsExactlyInAnyOrder("t1", + "t2"); + } + + private static class CountingFinder extends TablesNamesFinder { + private final Map visits = new HashMap<>(); + + @Override + public Void visit(Table table, S context) { + visits.merge(table.getFullyQualifiedName(), 1, Integer::sum); + return super.visit(table, context); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/TemporalExpressionInfoTest.java b/src/test/java/net/sf/jsqlparser/util/TemporalExpressionInfoTest.java new file mode 100644 index 0000000000..28395849be --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/TemporalExpressionInfoTest.java @@ -0,0 +1,149 @@ +/*- + * #%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.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.TimeKeyExpression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.TemporalExpressionInfo.Kind; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +class TemporalExpressionInfoTest { + + // Accepted forms from an execution matrix on MySQL 8.4.11 and PostgreSQL 18.6. + @ParameterizedTest + @CsvFileSource(resources = "temporal-expression-cases.tsv", delimiter = '\t') + void recognizesServerAcceptedFormsWithoutChangingTheirAst(Dialect dialect, String expression, + Kind kind, Integer precision) throws JSQLParserException { + PlainSelect select = parse(expression, dialect); + Expression node = select.getSelectItem(0).getExpression(); + String original = select.toString(); + TemporalExpressionInfo info = TemporalExpressionInfo.from(node, dialect).orElseThrow(); + assertEquals(kind, info.getKind()); + assertEquals(precision, info.getPrecision()); + assertEquals(expression.contains("("), info.hasParentheses()); + assertEquals(expression.split("\\(")[0], info.getName()); + assertEquals(original, select.toString()); + + StringBuilder visitor = new StringBuilder(); + select.accept(new StatementDeParser(visitor)); + for (String sql : List.of(select.toString(), visitor.toString())) { + PlainSelect reparsed = (PlainSelect) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(dialect)); + Expression again = reparsed.getSelectItem(0).getExpression(); + assertEquals(node.getClass(), again.getClass()); + TemporalExpressionInfo after = + TemporalExpressionInfo.from(again, dialect).orElseThrow(); + assertEquals(kind, after.getKind()); + assertEquals(precision, after.getPrecision()); + assertEquals(info.hasParentheses(), after.hasParentheses()); + } + } + + @Test + void preservesLegacyNodeClassesAndNormalizesTheirMetadata() throws JSQLParserException { + assertEquals(TimeKeyExpression.class, + node("CURRENT_TIMESTAMP", Dialect.POSTGRESQL).getClass()); + assertEquals(Function.class, + node("CURRENT_TIMESTAMP(6)", Dialect.POSTGRESQL).getClass()); + assertEquals(Column.class, node("LOCALTIME", Dialect.POSTGRESQL).getClass()); + assertEquals(Kind.LOCAL_TIME, info("LOCALTIME", Dialect.POSTGRESQL).getKind()); + assertEquals(Kind.CURRENT_TIMESTAMP, info("LOCALTIME", Dialect.MYSQL).getKind()); + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "POSTGRESQL"}) + void distinguishesOmittedAndZeroPrecisionAndUnwrapsGrouping(Dialect dialect) + throws JSQLParserException { + assertNull(info("CURRENT_TIMESTAMP", dialect).getPrecision()); + assertEquals(0, info("CURRENT_TIMESTAMP(0)", dialect).getPrecision()); + assertEquals(6, info("((current_timestamp(6)))", dialect).getPrecision()); + assertEquals("current_timestamp", info("((current_timestamp(6)))", dialect).getName()); + } + + @ParameterizedTest + @ValueSource(strings = {"\"LOCALTIME\"", "t.localtime", "t.current_timestamp", + "app.now()", "\"now\"()", "'CURRENT_TIMESTAMP'", "CURRENT_TIMEZONE", + "CURRENT_TIMESTAMP + INTERVAL '1' HOUR", "COALESCE(CURRENT_TIMESTAMP, NULL)", + "CURRENT_TIMESTAMP()", "CURRENT_DATE(1)", "CURRENT_TIMESTAMP('6')", + "CURRENT_TIMESTAMP(1, 2)", "CURRENT_TIMESTAMP(-1)", "NOW(6)", "CURDATE()"}) + void doesNotClassifyUnrelatedOrUnsupportedPostgreSqlExpressions(String expression) + throws JSQLParserException { + assertTrue(TemporalExpressionInfo.from(node(expression, Dialect.POSTGRESQL), + Dialect.POSTGRESQL).isEmpty(), expression); + } + + @ParameterizedTest + @ValueSource(strings = {"`LOCALTIME`", "\"LOCALTIME\"", "t.localtime", "app.now()", + "'NOW()'", "CURRENT_DATE(6)", "CURRENT_TIMESTAMP(1 + 1)", + "CURRENT_TIMESTAMP(1.5)", "CURRENT_TIMESTAMP(?)", "CURDATE(1)"}) + void doesNotClassifyUnrelatedOrUnsupportedMySqlExpressions(String expression) + throws JSQLParserException { + assertTrue(TemporalExpressionInfo.from(node(expression, Dialect.MYSQL), + Dialect.MYSQL).isEmpty(), expression); + } + + @Test + void recognizesRequestedPrecisionWithoutApplyingDatabaseRangeRules() + throws JSQLParserException { + // PostgreSQL accepts 7 with a warning and clamps to 6; MySQL rejects 7. Metadata retains + // the requested value, rather than pretending to perform execution-time validation. + assertEquals(7, info("CURRENT_TIMESTAMP(7)", Dialect.POSTGRESQL).getPrecision()); + assertEquals(7, info("CURRENT_TIMESTAMP(7)", Dialect.MYSQL).getPrecision()); + } + + @Test + void returnsSnapshotsAndIgnoresFunctionModifiers() { + Function function = new Function("CURRENT_TIMESTAMP", new LongValue(6)); + TemporalExpressionInfo before = TemporalExpressionInfo.from(function, Dialect.MYSQL) + .orElseThrow(); + ((LongValue) function.getParameters().get(0)).setValue(0); + assertEquals(6, before.getPrecision()); + assertEquals(0, TemporalExpressionInfo.from(function, Dialect.MYSQL) + .orElseThrow().getPrecision()); + function.setDistinct(true); + assertTrue(TemporalExpressionInfo.from(function, Dialect.MYSQL).isEmpty()); + assertTrue(TemporalExpressionInfo.from(new Column("NOW"), Dialect.POSTGRESQL).isEmpty()); + assertTrue(TemporalExpressionInfo.from(new Column("CURTIME"), Dialect.MYSQL).isEmpty()); + assertTrue(TemporalExpressionInfo.from(null, Dialect.MYSQL).isEmpty()); + assertTrue(TemporalExpressionInfo.from(function, Dialect.ORACLE).isEmpty()); + } + + private static TemporalExpressionInfo info(String expression, Dialect dialect) + throws JSQLParserException { + return TemporalExpressionInfo.from(node(expression, dialect), dialect).orElseThrow(); + } + + private static Expression node(String expression, Dialect dialect) throws JSQLParserException { + return parse(expression, dialect).getSelectItem(0).getExpression(); + } + + private static PlainSelect parse(String expression, Dialect dialect) + throws JSQLParserException { + return (PlainSelect) CCJSqlParserUtil.parse("SELECT " + expression, + p -> p.withDialect(dialect)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/UpdateJoinTablesTest.java b/src/test/java/net/sf/jsqlparser/util/UpdateJoinTablesTest.java new file mode 100644 index 0000000000..f2d5055747 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/UpdateJoinTablesTest.java @@ -0,0 +1,54 @@ +/*- + * #%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.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import java.util.Set; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.update.Update; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class UpdateJoinTablesTest { + @ParameterizedTest + @ValueSource(strings = { + "UPDATE target t JOIN source s ON s.id IN (SELECT id FROM hidden) SET t.a = 1", + "UPDATE target t LEFT JOIN source s ON EXISTS (SELECT 1 FROM hidden h WHERE h.id = s.id) SET t.a = 1", + "UPDATE target SET a = 1 FROM source s JOIN target t ON s.id IN (SELECT id FROM hidden)", + "WITH h AS (SELECT id FROM hidden) UPDATE target t JOIN source s ON s.id IN (SELECT id FROM h) SET t.a = 1"}) + void includesTablesInsideJoinConditions(String sql) throws Exception { + assertEquals(Set.of("target", "source", "hidden"), TablesNamesFinder.findTables(sql)); + } + + @Test + void preservesContextAndVisitsEachSourceOnce() throws Exception { + Update update = (Update) CCJSqlParserUtil.parse( + "UPDATE target t JOIN source s ON s.id IN (SELECT id FROM hidden) SET t.a = 1"); + Object context = new Object(); + java.util.List seen = new java.util.ArrayList<>(); + TablesNamesFinder finder = new TablesNamesFinder() { + { + init(false); + } + + @Override + public Void visit(Table table, S actual) { + assertSame(context, actual); + seen.add(table.getName()); + return null; + } + }; + update.accept(finder, context); + assertEquals(java.util.List.of("target", "source", "hidden"), seen); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParserTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParserTest.java new file mode 100644 index 0000000000..41428e00b1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParserTest.java @@ -0,0 +1,42 @@ +/*- + * #%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.util.deparser; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.policy.CreatePolicy; + +import org.junit.jupiter.api.Test; + +public class CreatePolicyDeParserTest { + + @Test + public void testUseExternalExpressionDeParser() throws JSQLParserException { + StringBuilder builder = new StringBuilder(); + ExpressionDeParser expressionDeParser = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + getBuilder().append('"').append(column.getColumnName()).append('"'); + return getBuilder(); + } + }; + expressionDeParser.setBuilder(builder); + + CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse( + "CREATE POLICY tenant_policy ON users AS RESTRICTIVE USING (tenant_id = owner_id)"); + new CreatePolicyDeParser(expressionDeParser, builder).deParse(policy); + + assertEquals("CREATE POLICY tenant_policy ON users AS RESTRICTIVE " + + "USING (\"tenant_id\" = \"owner_id\")", builder.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/DmlReturningExpressionTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/DmlReturningExpressionTest.java new file mode 100644 index 0000000000..a5f247f06f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/DmlReturningExpressionTest.java @@ -0,0 +1,57 @@ +/*- + * #%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.util.deparser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DmlReturningExpressionTest { + @ParameterizedTest + @ValueSource(strings = {"UPDATE t SET a = 7 RETURNING a + 8 AS result", + "DELETE FROM t WHERE a = 7 RETURNING a + 8 AS result", + "INSERT INTO t(a) VALUES (7) RETURNING a + 8 AS result", + "UPDATE t SET a = 7 RETURNING (SELECT b + 8 FROM source) AS result"}) + void visitsReturnedExpressionsExactlyOnce(String sql) throws Exception { + var statement = CCJSqlParserUtil.parse(sql); + String original = statement.toString(); + List seen = new ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + seen.add(value.getValue()); + return getBuilder().append(value.getValue() + 100); + } + }; + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(List.of(7L, 8L), seen); + assertEquals(original.replace("7", "107").replace("8", "108"), output.toString()); + assertEquals(original, statement.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"INSERT INTO t(a) VALUES (1) RETURNING *", + "DELETE FROM t RETURNING t.*, a AS result", + "UPDATE t SET a = 1 RETURNING a, b INTO x, y", + "DELETE FROM t RETURNING WITH (OLD AS o, NEW AS n) o.*, n.id"}) + void preservesWildcardsAliasesAndOutputTargets(String sql) throws Exception { + var statement = CCJSqlParserUtil.parse(sql); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/DmlSourceExpressionTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/DmlSourceExpressionTest.java new file mode 100644 index 0000000000..b4aa25757a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/DmlSourceExpressionTest.java @@ -0,0 +1,121 @@ +/*- + * #%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.util.deparser; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.statement.delete.Delete; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DmlSourceExpressionTest { + private static Statement parse(String sql) throws Exception { + return CCJSqlParserUtil.parse(sql, parser -> { + if (sql.startsWith("UPDATE t FROM")) { + parser.withDialect(net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect.TERADATA); + } + }); + } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE t JOIN s ON s.a = 7 SET t.a = 8 WHERE t.b = 9", + "UPDATE t SET a = 8 FROM (SELECT 7 AS a) s JOIN q ON s.a = q.a AND q.b = 9", + "UPDATE t FROM s JOIN q ON q.a = 7 SET t.a = 8", + "WITH c AS (SELECT 7 AS a), d AS (SELECT 8 AS b) UPDATE t SET a = 9 FROM c", + "WITH c AS (DELETE FROM s WHERE a = 7 RETURNING a) UPDATE t SET a = 8", + "WITH c AS (UPDATE s SET a = 7 RETURNING a) DELETE FROM t WHERE a = 8", + "WITH c AS (INSERT INTO s (a) VALUES (7) RETURNING a) DELETE FROM t WHERE a = 8", + "DELETE t FROM t JOIN s ON s.a = 7 WHERE t.b = 8", + "DELETE FROM t USING (SELECT 7 AS a) s WHERE t.a = 8", + "DELETE FROM t USING s, (SELECT 7 AS a) q WHERE t.a = 8" + }) + void visitsEverySourceExpressionOnceAndLeavesTheAstUnchanged(String sql) throws Exception { + Statement statement = parse(sql); + String original = statement.toString(); + for (boolean direct : new boolean[] {false, true}) { + StringBuilder output = new StringBuilder(); + List visited = new ArrayList<>(); + SelectDeParser select = new SelectDeParser(); + ExpressionDeParser expression = new ExpressionDeParser(select, output) { + @Override + public StringBuilder visit(LongValue value, S context) { + visited.add(value.getValue()); + return output.append(value.getValue() + 100); + } + }; + select.setExpressionVisitor(expression); + select.setBuilder(output); + if (direct && statement instanceof Update) { + new UpdateDeParser(expression, output).deParse((Update) statement); + } else if (direct) { + new DeleteDeParser(expression, output).deParse((Delete) statement); + } else { + statement.accept(new StatementDeParser(expression, select, output), null); + } + String expected = sql.replace("7", "107").replace("8", "108").replace("9", "109"); + assertEquals(parse(expected).toString(), + parse(output.toString()).toString()); + for (long value : new long[] {7, 8, 9}) { + assertEquals(sql.contains(Long.toString(value)) ? 1 : 0, + visited.stream().filter(v -> v == value).count()); + } + assertEquals(original, statement.toString()); + } + } + + @Test + void usesTheConfiguredSelectDeParserForDmlSources() throws Exception { + StringBuilder output = new StringBuilder(); + List tables = new ArrayList<>(); + SelectDeParser selects = new SelectDeParser() { + @Override + public StringBuilder visit(Table table, S context) { + tables.add(table.getName()); + return super.visit(table, context); + } + }; + parse("WITH c AS (SELECT a FROM hidden) UPDATE t " + + "SET a = 1 FROM source JOIN other ON source.a = other.a") + .accept(new StatementDeParser(new ExpressionDeParser(), selects, output), null); + assertEquals(List.of("hidden", "source", "other"), tables); + } + + @Test + void existingSettersAndGenericExpressionVisitorsCanRenderWithItems() throws Exception { + StringBuilder output = new StringBuilder(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter<>() { + @Override + public StringBuilder visit(LongValue value, S context) { + return output.append(value.getValue() + 100); + } + + @Override + public StringBuilder visit(Column column, S context) { + return output.append(column); + } + }; + UpdateDeParser deparser = new UpdateDeParser(); + deparser.setBuilder(output); + deparser.setExpressionVisitor(expressions); + deparser.deParse( + (Update) parse("WITH c AS (SELECT 7) UPDATE t SET a = 8")); + assertEquals("WITH c AS (SELECT 107) UPDATE t SET a = 108", output.toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/ExpressionContextTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/ExpressionContextTest.java new file mode 100644 index 0000000000..7ebc9af17f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/ExpressionContextTest.java @@ -0,0 +1,89 @@ +/*- + * #%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.util.deparser; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.Limit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ExpressionContextTest { + @ParameterizedTest + @ValueSource(strings = {"7 + 8 * 9", "COALESCE(7, ABS(8 + 9))", + "CASE WHEN 7 < 8 AND 9 <> 10 THEN 11 ELSE 12 END", + "(7, 8, 9)", "7 IN (8, 9)", "7 BETWEEN 8 AND 9", + "7 / 8 - 9 % 10", "7 || 8", "7 & 8 | 9 ^ 10", + "sum(7) FILTER (WHERE 8 > 9) OVER (PARTITION BY 10 ORDER BY 11)", + "array_agg(7 ORDER BY 8 LIMIT 9)"}) + void preservesCallerContextThroughNestedExpressions(String sql) throws Exception { + Object context = new Object(); + List seen = new ArrayList<>(); + StringBuilder output = new StringBuilder(); + ExpressionDeParser visitor = visitor(output, seen, context); + Expression expression = CCJSqlParserUtil.parseExpression(sql); + expression.accept(visitor, context); + assertFalse(seen.isEmpty()); + assertEquals(expression.toString(), output.toString()); + assertEquals(output.toString(), + CCJSqlParserUtil.parseExpression(output.toString()).toString()); + } + + @Test + void helpersRetainLegacyNullContextAndForwardExplicitContext() { + Object context = new Object(); + List seen = new ArrayList<>(); + StringBuilder out = new StringBuilder(); + ParenthesedExpressionList values = + new ParenthesedExpressionList<>(new LongValue(7), new LongValue(8)); + new ExpressionListDeParser<>(visitor(out, seen, context), out).deParse(values, context); + assertEquals(List.of(7L, 8L), seen); + out.setLength(0); + seen.clear(); + new ExpressionListDeParser<>(visitor(out, seen, null), out).deParse(values); + assertEquals("(7, 8)", out.toString()); + Limit limit = new Limit().withOffset(new LongValue(2)).withRowCount(new LongValue(3)); + out.setLength(0); + seen.clear(); + new LimitDeparser(visitor(out, seen, context), out).deParse(limit, context); + assertEquals(List.of(2L, 3L), seen); + out.setLength(0); + new LimitDeparser(visitor(out, new ArrayList<>(), null), out).deParse(limit); + assertEquals(" LIMIT 2, 3", out.toString()); + } + + @Test + void legacyExpressionEntryPointStillUsesNullContext() throws Exception { + StringBuilder out = new StringBuilder(); + List seen = new ArrayList<>(); + CCJSqlParserUtil.parseExpression("COALESCE(7 + 8, 9)") + .accept(visitor(out, seen, null)); + assertEquals(List.of(7L, 8L, 9L), seen); + } + + private static ExpressionDeParser visitor(StringBuilder out, List seen, Object expected) { + ExpressionDeParser visitor = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + assertSame(expected, context); + seen.add(value.getValue()); + return getBuilder().append(value.getValue()); + } + }; + visitor.setBuilder(out); + return visitor; + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/JsonExpressionRenderingTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/JsonExpressionRenderingTest.java new file mode 100644 index 0000000000..d824f6b861 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/JsonExpressionRenderingTest.java @@ -0,0 +1,298 @@ +/*- + * #%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.util.deparser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.JsonFunction; +import net.sf.jsqlparser.expression.JsonFunctionExpression; +import net.sf.jsqlparser.expression.JsonFunctionType; +import net.sf.jsqlparser.expression.JsonKeyValuePair; +import net.sf.jsqlparser.expression.JsonKeyValuePairSeparator; +import net.sf.jsqlparser.expression.JsonTableFunction; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class JsonExpressionRenderingTest { + private static final Object CONTEXT = new Object(); + + @ParameterizedTest + @ValueSource(strings = { + "JSON_VALUE(doc, '$.a' RETURNING CHAR(20) DEFAULT 'old' ON EMPTY DEFAULT 'old' ON ERROR)", + "JSON_QUERY(doc, '$.a' PASSING filter_value RETURNING VARCHAR(20) " + + "FORMAT JSON WITH ARRAY WRAPPER EMPTY ARRAY ON EMPTY ERROR ON ERROR)", + "JSON_EXISTS(doc, '$.a' PASSING filter_value UNKNOWN ON ERROR)", + "JSON_ARRAY(doc, JSON_VALUE(other_doc, '$.a') FORMAT JSON)", + "JSON_OBJECT(KEY key_column VALUE JSON_VALUE(doc, '$.a') FORMAT JSON)" + }) + void rewritesScalarChildrenWithoutMutatingTheAst(String sql) throws Exception { + Expression expression = CCJSqlParserUtil.parseExpression(sql); + String before = expression.toString(); + RecordingDeParser printer = new RecordingDeParser(); + expression.accept(printer, CONTEXT); + String output = printer.getBuilder().toString(); + assertEquals(before.replace("key_column", "r_key_column") + .replace("other_doc", "other_payload").replace("doc", "payload") + .replace("filter_value", "r_filter_value").replace("'$.a'", "'$.b'") + .replace("'old'", "'new'"), output); + assertEquals(output, CCJSqlParserUtil.parseExpression(output).toString()); + assertEquals(before, expression.toString()); + assertTrue(printer.columns.contains("doc")); + assertTrue(printer.strings.contains("$.a")); + } + + @Test + void rewritesMySqlGeneratedColumnExpressions() throws Exception { + String sql = "CREATE TABLE t (doc JSON, payload JSON, value VARCHAR(20) AS " + + "(JSON_VALUE(doc, '$.a' RETURNING CHAR(20) DEFAULT 'old' ON EMPTY)) STORED)"; + Statement statement = CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + String before = statement.toString(); + RecordingDeParser printer = new RecordingDeParser(null); + statement.accept(new StatementDeParser(printer, new SelectDeParser(), printer.getBuilder()), + null); + String output = printer.getBuilder().toString(); + assertEquals(before.replace("JSON_VALUE(doc", "JSON_VALUE(payload") + .replace("'$.a'", "'$.b'").replace("'old'", "'new'"), output); + assertEquals(List.of("doc"), printer.columns); + assertEquals(output, CCJSqlParserUtil.parse(output, + p -> p.withDialect(Dialect.MYSQL)).toString()); + assertEquals(before, statement.toString()); + } + + @Test + void rewritesNestedTablePathsDefaultsAndPassingValuesInSourceOrder() throws Exception { + String sql = "SELECT * FROM JSON_TABLE(doc, '$.a' AS root " + + "PASSING filter_value AS filter COLUMNS (" + + "ord FOR ORDINALITY, value VARCHAR(20) PATH '$.a' " + + "DEFAULT 'old' ON ERROR DEFAULT 'old' ON EMPTY, " + + "NESTED PATH '$.a' AS nested COLUMNS (present INT EXISTS PATH '$.a')) " + + "PLAN (root OUTER nested) EMPTY ON ERROR) AS jt"; + Statement statement = CCJSqlParserUtil.parse(sql); + String before = statement.toString(); + RecordingDeParser printer = new RecordingDeParser(); + statement.accept(new StatementDeParser(printer, new SelectDeParser(), printer.getBuilder()), + CONTEXT); + String output = printer.getBuilder().toString(); + assertEquals(before.replace("JSON_TABLE(doc", "JSON_TABLE(payload") + .replace("filter_value", "r_filter_value").replace("'$.a'", "'$.b'") + .replace("'old'", "'new'"), output); + assertEquals(List.of("doc", "filter_value"), printer.columns); + assertEquals(List.of("$.a", "$.a", "old", "old", "$.a", "$.a"), printer.strings); + assertEquals(output, CCJSqlParserUtil.parse(output).toString()); + assertEquals(before, statement.toString()); + } + + @Test + void passesMySqlJsonTableDefaultsThroughTheExpressionPrinter() throws Exception { + String sql = "SELECT * FROM JSON_TABLE(doc, '$.a' COLUMNS " + + "(value VARCHAR(20) PATH '$.a' DEFAULT 'old' ON EMPTY " + + "DEFAULT 'old' ON ERROR)) AS jt"; + Statement statement = CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + RecordingDeParser printer = new RecordingDeParser(); + statement.accept(new StatementDeParser(printer, new SelectDeParser(), printer.getBuilder()), + CONTEXT); + String expected = statement.toString().replace("JSON_TABLE(doc", "JSON_TABLE(payload") + .replace("'$.a'", "'$.b'").replace("'old'", "'new'"); + assertEquals(expected, printer.getBuilder().toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected, + p -> p.withDialect(Dialect.MYSQL)).toString()); + } + + @Test + void rendersExpressionPlanTermsWithoutRewritingPathNames() { + JsonTableFunction.JsonTablePlanExpression nested = + new JsonTableFunction.JsonTablePlanExpression() + .addTerm(new JsonTableFunction.JsonTablePlanTerm() + .setExpression(new Column("doc"))); + JsonTableFunction.JsonTablePlanExpression plan = + new JsonTableFunction.JsonTablePlanExpression() + .addTerm(new JsonTableFunction.JsonTablePlanTerm().setName("doc")) + .addOperator(JsonTableFunction.JsonTablePlanOperator.OUTER) + .addTerm(new JsonTableFunction.JsonTablePlanTerm() + .setNestedPlanExpression(nested)); + RecordingDeParser printer = new RecordingDeParser(); + plan.appendTo(printer.getBuilder(), expression -> expression.accept(printer, CONTEXT)); + assertEquals("doc OUTER (payload)", printer.getBuilder().toString()); + assertEquals(List.of("doc"), printer.columns); + assertEquals("doc OUTER (doc)", plan.toString()); + } + + @Test + void leavesOpaqueKeyValueObjectsAndAdditionalQueryPathsUnchanged() throws Exception { + JsonKeyValuePair pair = new JsonKeyValuePair("raw_key", "raw_value", false, + JsonKeyValuePairSeparator.COLON); + RecordingDeParser printer = new RecordingDeParser(); + pair.appendTo(printer.getBuilder(), expression -> expression.accept(printer, CONTEXT)); + assertEquals(pair.toString(), printer.getBuilder().toString()); + assertTrue(printer.columns.isEmpty()); + JsonFunction query = (JsonFunction) CCJSqlParserUtil.parseExpression( + "JSON_QUERY(doc, '$.a' WITH ARRAY WRAPPER, '$.a')"); + RecordingDeParser queryPrinter = new RecordingDeParser(); + query.accept(queryPrinter, CONTEXT); + assertEquals("JSON_QUERY(payload, '$.b' WITH ARRAY WRAPPER, '$.a')", + queryPrinter.getBuilder().toString()); + assertEquals(List.of("$.a"), queryPrinter.strings); + } + + @Test + void rewritesScalarSubqueriesThroughTheConnectedStatementDeparser() throws Exception { + Statement statement = CCJSqlParserUtil.parse( + "SELECT JSON_VALUE((SELECT doc FROM source_table), '$.a')"); + String before = statement.toString(); + RecordingDeParser printer = new RecordingDeParser(null); + statement.accept(new StatementDeParser(printer, new SelectDeParser(), printer.getBuilder()), + null); + + String expected = before.replace("SELECT doc", "SELECT payload") + .replace("'$.a'", "'$.b'"); + assertEquals(expected, printer.getBuilder().toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected).toString()); + assertEquals(List.of("doc"), printer.columns); + assertEquals(before, statement.toString()); + } + + @Test + void preservesNullPassingExpressionsInIncompleteScalarBuilders() throws Exception { + JsonFunction function = (JsonFunction) CCJSqlParserUtil.parseExpression( + "JSON_VALUE(doc, '$.a')"); + function.addPassingExpression(null); + function.addPassingExpression(new Column("filter_value")); + RecordingDeParser printer = new RecordingDeParser(); + function.accept(printer, CONTEXT); + + assertEquals("JSON_VALUE(doc, '$.a' PASSING null, filter_value)", function.toString()); + assertEquals("JSON_VALUE(payload, '$.b' PASSING null, r_filter_value)", + printer.getBuilder().toString()); + assertEquals(List.of("doc", "filter_value"), printer.columns); + } + + @Test + void preservesNullPassingClausesAndPlanTermsInIncompleteTableBuilders() { + JsonTableFunction function = new JsonTableFunction() + .setJsonInputExpression(new Column("doc")) + .addPassingClause(new JsonTableFunction.JsonTablePassingClause( + new Column("filter_value"), "filter")) + .setPlanClause(new JsonTableFunction.JsonTablePlanClause().setPlanExpression( + new JsonTableFunction.JsonTablePlanExpression().addTerm(null) + .addOperator(JsonTableFunction.JsonTablePlanOperator.OUTER) + .addTerm(null))); + function.getPassingClauses().add(0, null); + RecordingDeParser printer = new RecordingDeParser(); + function.accept(printer, CONTEXT); + + assertEquals("JSON_TABLE(doc PASSING null, filter_value AS filter null " + + "PLAN (null OUTER null))", function.toString()); + assertEquals("JSON_TABLE(payload PASSING null, r_filter_value AS filter null " + + "PLAN (null OUTER null))", printer.getBuilder().toString()); + assertEquals(List.of("doc", "filter_value"), printer.columns); + } + + @Test + void scalarRendererOverloadsCustomizeBothDefaultAndVisitorOutput() { + JsonFunction function = new JsonFunction(JsonFunctionType.VALUE) { + @Override + public StringBuilder appendValue(StringBuilder builder, + Consumer expressionPrinter) { + return super.appendValue(builder, expressionPrinter).append(" /* custom */"); + } + }; + function.setInputExpression(new JsonFunctionExpression(new Column("doc"))); + function.setJsonPathExpression(new StringValue("$.a")); + RecordingDeParser printer = new RecordingDeParser(); + function.accept(printer, CONTEXT); + + assertEquals("JSON_VALUE(doc, '$.a') /* custom */", function.toString()); + assertEquals("JSON_VALUE(payload, '$.b') /* custom */", printer.getBuilder().toString()); + assertEquals(List.of("doc"), printer.columns); + } + + @Test + void columnAppendHookAndOpaqueCustomColumnsWorkInBothRenderers() { + JsonTableFunction.JsonTableColumnDefinition opaque = + new JsonTableFunction.JsonTableColumnDefinition() { + @Override + public void collectExpressions(List expressions) {} + + @Override + public String toString() { + return "legacy FOR ORDINALITY"; + } + }; + JsonTableFunction.JsonTableValueColumnDefinition structured = + new JsonTableFunction.JsonTableValueColumnDefinition() { + @Override + public StringBuilder appendTo(StringBuilder builder, + Consumer expressionPrinter) { + return super.appendTo(builder, expressionPrinter).append(" /* custom */"); + } + }; + structured.setColumnName("value").setPathExpression(new StringValue("$.a")); + JsonTableFunction function = new JsonTableFunction() + .setJsonInputExpression(new Column("doc")) + .setColumnsClause(new JsonTableFunction.JsonTableColumnsClause() + .addColumnDefinition(opaque).addColumnDefinition(structured)); + RecordingDeParser printer = new RecordingDeParser(); + function.accept(printer, CONTEXT); + + assertEquals("JSON_TABLE(doc COLUMNS (legacy FOR ORDINALITY, " + + "value PATH '$.a' /* custom */))", function.toString()); + assertEquals("JSON_TABLE(payload COLUMNS (legacy FOR ORDINALITY, " + + "value PATH '$.b' /* custom */))", printer.getBuilder().toString()); + assertEquals(List.of("doc"), printer.columns); + assertEquals(List.of("$.a"), printer.strings); + } + + private static class RecordingDeParser extends ExpressionDeParser { + private final List columns = new ArrayList<>(); + private final List strings = new ArrayList<>(); + private final Object expectedContext; + + RecordingDeParser() { + this(CONTEXT); + } + + RecordingDeParser(Object expectedContext) { + this.expectedContext = expectedContext; + } + + @Override + public StringBuilder visit(Column column, S context) { + assertSame(expectedContext, context); + columns.add(column.getColumnName()); + String name = column.getColumnName(); + String replacement = "doc".equals(name) ? "payload" + : "other_doc".equals(name) ? "other_payload" : "r_" + name; + return super.visit(new Column(column.getTable(), replacement), context); + } + + @Override + public StringBuilder visit(StringValue value, S context) { + assertSame(expectedContext, context); + strings.add(value.getValue()); + if ("$.a".equals(value.getValue()) || "old".equals(value.getValue())) { + return super.visit(new StringValue("$.a".equals(value.getValue()) ? "$.b" : "new"), + context); + } + return super.visit(value, context); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/StatementListDeParserTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/StatementListDeParserTest.java new file mode 100644 index 0000000000..94c434d6e1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/StatementListDeParserTest.java @@ -0,0 +1,249 @@ +/*- + * #%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.util.deparser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Block; +import net.sf.jsqlparser.statement.IfElseStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.select.Select; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class StatementListDeParserTest { + @Test + void rendersEmptyListIntoExistingBuilder() { + StringBuilder output = new StringBuilder("prefix"); + Statements statements = new Statements(); + assertSame(output, statements.accept(new StatementDeParser(output), null)); + assertEquals("prefix", output.toString()); + assertEquals("", statements.toString()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "SELECT 1 | SELECT 1;", + "SELECT 1; SELECT 2; | SELECT 1; SELECT 2;", + ";;SELECT 1;;; SELECT 2;; | SELECT 1; SELECT 2;" + }) + void rendersEveryStatementWithSeparators(String sql, String expected) throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements(sql); + StringBuilder output = new StringBuilder(); + assertSame(output, statements.accept(new StatementDeParser(output), null)); + assertEquals(expected.replace("; ", ";\n") + "\n", output.toString()); + assertEquals(statements.toString(), output.toString()); + assertRoundTrip(statements, output.toString(), null); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (id integer); INSERT INTO t VALUES (1); SELECT id FROM t;", + "BEGIN SELECT 1; END;", + "BEGIN SELECT 1; END", + "SELECT 0; BEGIN SELECT 1; END;", + "IF 1 = 1 SELECT 2; SELECT 3;", + "IF 1 = 1 SELECT 2; ELSE SELECT 3; SELECT 4;", + "IF 1 = 1 BEGIN SELECT 2; END ELSE BEGIN SELECT 3; END; SELECT 4;", + "IF 1 = 1 BEGIN SELECT 2; SELECT 3; END;" + }) + void preservesCompoundStatementBoundaries(String sql) throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements(sql); + String output = render(statements); + assertEquals(statements.toString(), output); + assertRoundTrip(statements, output, null); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void blockOwnsItsEndSemicolon(boolean terminated) throws Exception { + Block block = new Block().withStatements(CCJSqlParserUtil.parseStatements("SELECT 1;")); + block.setSemicolonAfterEnd(terminated); + Statements statements = new Statements(); + statements.add(block); + statements.add(CCJSqlParserUtil.parse("SELECT 2")); + String expected = "BEGIN\nSELECT 1;\nEND" + (terminated ? ";" : "") + "\nSELECT 2;\n"; + assertEquals(expected, render(statements)); + assertEquals(expected, statements.toString()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void rendersBlocksWithoutStatements(boolean terminated) { + Block block = new Block(); + block.setSemicolonAfterEnd(terminated); + Statements statements = new Statements(); + statements.add(block); + String expected = "BEGIN\nEND" + (terminated ? ";" : "") + "\n"; + assertEquals(expected, render(statements)); + block.setStatements(new Statements()); + assertEquals(expected, render(statements)); + } + + @Test + void nestedBlocksDoNotAddSeparatorsAfterTerminatedChildren() throws Exception { + Statements body = CCJSqlParserUtil.parseStatements("IF 1 = 1 SELECT 2; ELSE SELECT 3;"); + Block inner = new Block().withStatements(CCJSqlParserUtil.parseStatements("SELECT 4;")); + inner.setSemicolonAfterEnd(true); + body.add(inner); + Block outer = new Block().withStatements(body); + outer.setSemicolonAfterEnd(true); + Statements statements = new Statements(); + statements.add(outer); + statements.add(CCJSqlParserUtil.parse("SELECT 5")); + String expected = "BEGIN\nIF 1 = 1 SELECT 2; ELSE SELECT 3;\n" + + "BEGIN\nSELECT 4;\nEND;\nEND;\nSELECT 5;\n"; + assertEquals(expected, render(statements)); + assertEquals(expected, statements.toString()); + } + + @ParameterizedTest + @CsvSource({"false,false", "false,true", "true,false", "true,true"}) + void ifElseOwnsItsBranchSemicolons(boolean thenTerminated, boolean elseTerminated) + throws Exception { + IfElseStatement conditional = new IfElseStatement( + CCJSqlParserUtil.parseCondExpression("1 = 1"), CCJSqlParserUtil.parse("SELECT 2")); + conditional.setElseStatement(CCJSqlParserUtil.parse("SELECT 3")); + conditional.setUsingSemicolonForIfStatement(thenTerminated); + conditional.setUsingSemicolonForElseStatement(elseTerminated); + Statements statements = new Statements(); + statements.add(conditional); + String expected = "IF 1 = 1 SELECT 2" + (thenTerminated ? ";" : "") + + " ELSE SELECT 3" + (elseTerminated ? ";" : "") + "\n"; + assertEquals(expected, render(statements)); + assertEquals(expected, statements.toString()); + } + + @Test + void appliesCustomVisitorsInsideBranchesAndNestedListsWithContext() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + "IF flag = 1 BEGIN SELECT 2; END ELSE BEGIN SELECT 3; END; SELECT 4;"); + String original = statements.toString(); + Object context = new Object(); + List visitedConditions = new ArrayList<>(); + List visitedValues = new ArrayList<>(); + List